mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
commit
f10594e013
6 changed files with 83 additions and 49 deletions
|
@ -91,14 +91,28 @@ export function formatTMDBMeta(
|
||||||
export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem {
|
export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem {
|
||||||
const type = TMDBMediaToMediaItemType(media.object_type);
|
const type = TMDBMediaToMediaItemType(media.object_type);
|
||||||
|
|
||||||
return {
|
// Define the basic structure of MediaItem
|
||||||
|
const mediaItem: MediaItem = {
|
||||||
title: media.title,
|
title: media.title,
|
||||||
id: media.id.toString(),
|
id: media.id.toString(),
|
||||||
year: media.original_release_date?.getFullYear() ?? 0,
|
year: media.original_release_date?.getFullYear() ?? 0,
|
||||||
release_date: media.original_release_date,
|
release_date: media.original_release_date,
|
||||||
poster: media.poster,
|
poster: media.poster,
|
||||||
type,
|
type,
|
||||||
|
seasons: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If it's a TV show, include the seasons information
|
||||||
|
if (type === "show") {
|
||||||
|
const seasons = media.seasons?.map((season) => ({
|
||||||
|
title: season.title,
|
||||||
|
id: season.id.toString(),
|
||||||
|
number: season.season_number,
|
||||||
|
}));
|
||||||
|
mediaItem.seasons = seasons as MWSeasonMeta[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TMDBIdToUrlId(
|
export function TMDBIdToUrlId(
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { get } from "@/backend/metadata/tmdb";
|
import { get } from "@/backend/metadata/tmdb";
|
||||||
|
import { Flare } from "@/components/utils/Flare";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
interface ModalEpisodeSelectorProps {
|
interface ModalEpisodeSelectorProps {
|
||||||
|
@ -57,8 +58,8 @@ export function EpisodeSelector({
|
||||||
}, [handleSeasonSelect, tmdbId]);
|
}, [handleSeasonSelect, tmdbId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row relative">
|
||||||
<div className="sm:w-96 w-96 sm:block cursor-pointer overflow-y-scroll overflow-x-hidden max-h-60 max-w-24">
|
<div className="w-24 sm:w-96 cursor-pointer overflow-y-auto overflow-x-hidden max-h-60 z-10">
|
||||||
{seasonsData.map((season) => (
|
{seasonsData.map((season) => (
|
||||||
<div
|
<div
|
||||||
key={season.season_number}
|
key={season.season_number}
|
||||||
|
@ -76,8 +77,8 @@ export function EpisodeSelector({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-auto mt-4 cursor-pointer sm:mt-0 sm:ml-4 overflow-y-auto overflow-x-hidden max-h-60 order-1 sm:order-2">
|
<div className="flex-auto mt-4 sm:mt-0 sm:ml-4 cursor-pointer overflow-x-auto overflow-y-hidden sm:overflow-y-auto sm:overflow-x-hidden max-h-60 max-w-[70vw] z-0">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="flex sm:grid sm:grid-cols-3 sm:gap-2">
|
||||||
{selectedSeason ? (
|
{selectedSeason ? (
|
||||||
selectedSeason.episodes.map(
|
selectedSeason.episodes.map(
|
||||||
(episode: {
|
(episode: {
|
||||||
|
@ -87,15 +88,22 @@ export function EpisodeSelector({
|
||||||
show_id: number;
|
show_id: number;
|
||||||
id: number;
|
id: number;
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<Flare.Base
|
||||||
key={episode.episode_number}
|
key={episode.episode_number}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
`/media/tmdb-tv-${tmdbId}-${mediaTitle}/${episode.show_id}/${episode.id}`,
|
`/media/tmdb-tv-${tmdbId}-${mediaTitle}/${episode.show_id}/${episode.id}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="bg-mediaCard-hoverBackground rounded p-2 hover:scale-95 transition-transform transition-border-color duration-[0.28s] ease-in-out transform-origin-center"
|
className="group cursor-pointer rounded-xl relative p-[0.65em] bg-background-main transition-colors duration-[0.28s] flex-shrink-0 w-48 sm:w-auto mr-2 sm:mr-0"
|
||||||
>
|
>
|
||||||
|
<Flare.Light
|
||||||
|
flareSize={300}
|
||||||
|
cssColorVar="--colors-mediaCard-hoverAccent"
|
||||||
|
backgroundClass="bg-mediaCard-hoverBackground duration-200"
|
||||||
|
className="rounded-xl bg-background-main group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
<div className="relative z-10">
|
||||||
<img
|
<img
|
||||||
src={`https://image.tmdb.org/t/p/w500/${episode.still_path}`}
|
src={`https://image.tmdb.org/t/p/w500/${episode.still_path}`}
|
||||||
className="w-full h-auto rounded"
|
className="w-full h-auto rounded"
|
||||||
|
@ -104,6 +112,7 @@ export function EpisodeSelector({
|
||||||
{episode.name}
|
{episode.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Flare.Base>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -20,6 +20,29 @@ interface PopupModalProps {
|
||||||
media: MediaItem;
|
media: MediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MediaData {
|
||||||
|
backdrop_path?: string;
|
||||||
|
title?: string;
|
||||||
|
name?: string;
|
||||||
|
runtime?: number;
|
||||||
|
release_date?: string;
|
||||||
|
first_air_date?: string;
|
||||||
|
vote_average?: number;
|
||||||
|
genres?: Array<{ name: string }>;
|
||||||
|
overview?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaInfo {
|
||||||
|
results?: Array<{
|
||||||
|
iso_3166_1: string;
|
||||||
|
rating?: string;
|
||||||
|
release_dates?: Array<{
|
||||||
|
certification: string;
|
||||||
|
release_date: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
type StyleState = {
|
type StyleState = {
|
||||||
opacity: number;
|
opacity: number;
|
||||||
visibility: "visible" | "hidden" | undefined;
|
visibility: "visible" | "hidden" | undefined;
|
||||||
|
@ -46,11 +69,10 @@ export function PopupModal({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
visibility: "hidden",
|
visibility: "hidden",
|
||||||
});
|
});
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<MediaData | null>(null);
|
||||||
const [mediaInfo, setMediaInfo] = useState<any>(null);
|
const [mediaInfo, setMediaInfo] = useState<MediaInfo | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||||
const [menuOpen, setMenuOpen] = useState<boolean>(false); // Added definition for menuOpen
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -62,7 +84,6 @@ export function PopupModal({
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
@ -83,7 +104,7 @@ export function PopupModal({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaTypePath = media.type === "show" ? "tv" : media.type;
|
const mediaTypePath = media.type === "show" ? "tv" : media.type;
|
||||||
const result = await get<any>(`/${mediaTypePath}/${media.id}`, {
|
const result = await get<MediaData>(`/${mediaTypePath}/${media.id}`, {
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: "en-US",
|
language: "en-US",
|
||||||
});
|
});
|
||||||
|
@ -107,7 +128,7 @@ export function PopupModal({
|
||||||
const mediaTypeForAPI = media.type === "show" ? "tv" : media.type;
|
const mediaTypeForAPI = media.type === "show" ? "tv" : media.type;
|
||||||
const endpointSuffix =
|
const endpointSuffix =
|
||||||
media.type === "show" ? "content_ratings" : "release_dates";
|
media.type === "show" ? "content_ratings" : "release_dates";
|
||||||
const result = await get<any>(
|
const result = await get<MediaInfo>(
|
||||||
`/${mediaTypeForAPI}/${media.id}/${endpointSuffix}`,
|
`/${mediaTypeForAPI}/${media.id}/${endpointSuffix}`,
|
||||||
{
|
{
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
|
@ -126,10 +147,7 @@ export function PopupModal({
|
||||||
}, [media.id, media.type, isVisible]);
|
}, [media.id, media.type, isVisible]);
|
||||||
|
|
||||||
if (!isVisible && style.visibility === "hidden") return null;
|
if (!isVisible && style.visibility === "hidden") return null;
|
||||||
|
if (error) return <div>Error: {error}</div>;
|
||||||
if (error) {
|
|
||||||
return <div>Error: {error}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTVShow = media.type === "show";
|
const isTVShow = media.type === "show";
|
||||||
const usReleaseInfo = mediaInfo?.results?.find(
|
const usReleaseInfo = mediaInfo?.results?.find(
|
||||||
|
@ -158,11 +176,7 @@ export function PopupModal({
|
||||||
<div
|
<div
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
className="rounded-xl bg-modal-background flex flex-col justify-center items-center transition-opacity duration-100 max-w-full sm:max-w-xl w-full sm:w-auto sm:h-auto overflow-y-auto p-4"
|
className="rounded-xl bg-modal-background flex flex-col justify-center items-center transition-opacity duration-100 max-w-full sm:max-w-xl w-full sm:w-auto sm:h-auto overflow-y-auto p-4"
|
||||||
style={{
|
style={{ opacity: style.opacity, maxHeight: "90vh", height: "auto" }}
|
||||||
opacity: style.opacity,
|
|
||||||
maxHeight: "90vh",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="aspect-w-16 aspect-h-9 w-full sm:w-auto">
|
<div className="aspect-w-16 aspect-h-9 w-full sm:w-auto">
|
||||||
<div className="rounded-xl">
|
<div className="rounded-xl">
|
||||||
|
@ -172,9 +186,7 @@ export function PopupModal({
|
||||||
alt={media.poster ? "" : "failed to fetch :("}
|
alt={media.poster ? "" : "failed to fetch :("}
|
||||||
className="rounded-xl object-cover w-full h-full"
|
className="rounded-xl object-cover w-full h-full"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{
|
style={{ maxHeight: "60vh" }}
|
||||||
maxHeight: "60vh",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
|
@ -239,8 +251,7 @@ export function PopupModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-3 pb-3 pt-2">
|
<div className="flex flex-row gap-3 pb-3 pt-2">
|
||||||
<div className="flex items-center space-x-[2px] font-semibold">
|
<div className="flex items-center space-x-[2px] font-semibold">
|
||||||
{Array.from({ length: 5 }, (_, index) => {
|
{Array.from({ length: 5 }, (_, index) => (
|
||||||
return (
|
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
{index < Math.round(Number(data?.vote_average) / 2) ? (
|
{index < Math.round(Number(data?.vote_average) / 2) ? (
|
||||||
<Icon icon={Icons.STAR} />
|
<Icon icon={Icons.STAR} />
|
||||||
|
@ -248,10 +259,9 @@ export function PopupModal({
|
||||||
<Icon icon={Icons.STAR_BORDER} />
|
<Icon icon={Icons.STAR_BORDER} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{data?.genres?.length > 0
|
{data?.genres && data.genres.length > 0
|
||||||
? data.genres.map((genre: { name: string }) => (
|
? data.genres.map((genre: { name: string }) => (
|
||||||
<div key={genre.name} className="inline-block">
|
<div key={genre.name} className="inline-block">
|
||||||
<div className="px-2 py-1 bg-mediaCard-hoverBackground rounded hover:bg-search-background cursor-default duration-200 transition-colors transform hover:scale-110">
|
<div className="px-2 py-1 bg-mediaCard-hoverBackground rounded hover:bg-search-background cursor-default duration-200 transition-colors transform hover:scale-110">
|
||||||
|
@ -259,8 +269,9 @@ export function PopupModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: Array.from({ length: 3 }).map((_) => (
|
: Array.from({ length: 3 }).map((_, idx) => (
|
||||||
<div className="inline-block">
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<div className="inline-block" key={idx}>
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -118,8 +118,6 @@ function EpisodesView({
|
||||||
const newData = setPlayerMeta(loadingState.value.fullData, episodeId);
|
const newData = setPlayerMeta(loadingState.value.fullData, episodeId);
|
||||||
if (newData) onChange?.(newData);
|
if (newData) onChange?.(newData);
|
||||||
}
|
}
|
||||||
// prevent router clear here, otherwise its done double
|
|
||||||
// player already switches route after meta change
|
|
||||||
router.close(true);
|
router.close(true);
|
||||||
},
|
},
|
||||||
[setPlayerMeta, loadingState, router, onChange],
|
[setPlayerMeta, loadingState, router, onChange],
|
||||||
|
|
|
@ -134,6 +134,7 @@ function App() {
|
||||||
</LegacyUrlView>
|
</LegacyUrlView>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/browse/:query?" element={<HomePage />} />
|
<Route path="/browse/:query?" element={<HomePage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export interface MediaItem {
|
export interface MediaItem {
|
||||||
|
seasons: import("c:/Users/huzei/OneDrive/Desktop/Sudo-Flix/src/backend/metadata/types/mw").MWSeasonMeta[];
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
|
|
Loading…
Reference in a new issue