mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
Migration A
This commit is contained in:
parent
c383dc1da4
commit
6575cb30a6
13 changed files with 306 additions and 49 deletions
|
|
@ -18,9 +18,11 @@
|
|||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" />
|
||||
<meta name="msapplication-TileColor" content="#120f1d" />
|
||||
<meta name="theme-color" content="#120f1d" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
|
||||
href="/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_landscape.png">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Lato", sans-serif !important;
|
||||
@apply bg-background-main font-main text-type-text;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
|
|
@ -79,6 +81,16 @@ html[data-no-scroll], html[data-no-scroll] body {
|
|||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
display: inline-block;
|
||||
padding: 0.75em;
|
||||
margin: -0.75em;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(-15px, -10px)
|
||||
}
|
||||
|
||||
/*generated with Input range slider CSS style generator (version 20211225)
|
||||
https://toughengineer.github.io/demo/slider-styler*/
|
||||
:root {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
|
@ -7,31 +7,48 @@ import { Icon, Icons } from "@/components/Icon";
|
|||
export interface EditButtonProps {
|
||||
editing: boolean;
|
||||
onEdit?: (editing: boolean) => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function EditButton(props: EditButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
props.onEdit?.(!props.editing);
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105"
|
||||
>
|
||||
<span ref={parent}>
|
||||
{props.editing ? (
|
||||
<span className="mx-2 sm:mx-4 whitespace-nowrap">
|
||||
{t("home.mediaList.stopEditing")}
|
||||
</span>
|
||||
) : (
|
||||
<Icon icon={Icons.EDIT} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105"
|
||||
id={props.id} // Assign id to the button
|
||||
>
|
||||
<span ref={parent}>
|
||||
{props.editing ? (
|
||||
<span className="mx-2 sm:mx-4 whitespace-nowrap">
|
||||
{t("home.mediaList.stopEditing")}
|
||||
</span>
|
||||
) : (
|
||||
<Icon icon={Icons.EDIT} />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{props.editing && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="fixed bottom-9 right-7 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-background-secondary text-white border-2 border-green-500 transition-[background-color,transform,box-shadow] hover:bg-background-secondaryHover hover:scale-110 cursor-pointer"
|
||||
id={props.id ? `${props.id}-check` : undefined} // Optionally use a different id for this button
|
||||
>
|
||||
<Icon icon={Icons.CHECKMARK} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export function WideContainer(props: WideContainerProps) {
|
|||
return (
|
||||
<div
|
||||
className={`mx-auto max-w-full px-8 ${
|
||||
props.ultraWide ? "w-[1300px] sm:px-16" : "w-[900px] sm:px-8"
|
||||
props.ultraWide
|
||||
? "w-[1300px] 2xl:w-[2000px] 3xl:w-[2400px] 4xl:w-[2800px]"
|
||||
: "w-[900px] 2xl:w-[1400px] 3xl:w-[1600px] 4xl:w-[1800px]"
|
||||
} ${props.classNames || ""}`}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { MediaItem } from "@/utils/mediaTypes";
|
|||
|
||||
import { MediaBookmarkButton } from "./MediaBookmark";
|
||||
import { IconPatch } from "../buttons/IconPatch";
|
||||
import { Icons } from "../Icon";
|
||||
import { Icon, Icons } from "../Icon";
|
||||
|
||||
export interface MediaCardProps {
|
||||
media: MediaItem;
|
||||
|
|
@ -179,7 +179,29 @@ function MediaCardContent({
|
|||
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
|
||||
<span>{media.title}</span>
|
||||
</h1>
|
||||
<DotList className="text-xs" content={dotListContent} />
|
||||
<div className="media-info-container justify-content-center flex flex-wrap">
|
||||
<DotList className="text-xs" content={dotListContent} />
|
||||
<button
|
||||
className="info-button"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const searchParam = encodeURIComponent(encodeURI(media.id));
|
||||
const url =
|
||||
media.type === "movie"
|
||||
? `https://www.themoviedb.org/movie/${searchParam}`
|
||||
: `https://www.themoviedb.org/tv/${searchParam}`;
|
||||
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="text-xs font-semibold text-type-secondary"
|
||||
icon={Icons.CIRCLE_QUESTION}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const MediaGrid = forwardRef<HTMLDivElement, MediaGridProps>(
|
|||
(props, ref) => {
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4"
|
||||
className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 2xl:grid-cols-6 3xl:grid-cols-8 4xl:grid-cols-10"
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ import { SubPageLayout } from "./layouts/SubPageLayout";
|
|||
import { Icon, Icons } from "../components/Icon";
|
||||
import { PageTitle } from "./parts/util/PageTitle";
|
||||
|
||||
const editorPicks = [
|
||||
{ id: 9342, type: "movie" }, // The Mask of Zorro
|
||||
{ id: 293, type: "movie" }, // A River Runs Through It
|
||||
{ id: 370172, type: "movie" }, // No Time To Die
|
||||
{ id: 661374, type: "movie" }, // The Glass Onion
|
||||
{ id: 207, type: "movie" }, // Dead Poets Society
|
||||
{ id: 378785, type: "movie" }, // The Best of the Blues Brothers
|
||||
{ id: 335984, type: "movie" }, // Blade Runner 2049
|
||||
{ id: 13353, type: "movie" }, // It's the Great Pumpkin, Charlie Brown
|
||||
{ id: 27205, type: "movie" }, // Inception
|
||||
{ id: 106646, type: "movie" }, // The Wolf of Wall Street
|
||||
{ id: 334533, type: "movie" }, // Captain Fantastic
|
||||
{ id: 693134, type: "movie" }, // Dune: Part Two
|
||||
{ id: 765245, type: "movie" }, // Swan Song
|
||||
{ id: 264660, type: "movie" }, // Ex Machina
|
||||
{ id: 92591, type: "movie" }, // Bernie
|
||||
{ id: 976893, type: "movie" }, // Perfect Days
|
||||
];
|
||||
|
||||
export function Discover() {
|
||||
const { t } = useTranslation();
|
||||
const [genres, setGenres] = useState<Genre[]>([]);
|
||||
|
|
@ -48,6 +67,45 @@ export function Discover() {
|
|||
const [countdownTimeout, setCountdownTimeout] =
|
||||
useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [editorPicksData, setEditorPicksData] = useState<Media[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Function to shuffle array
|
||||
const shuffleArray = (array: any[]) => {
|
||||
for (let i = array.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
const fetchEditorPicks = async () => {
|
||||
try {
|
||||
// Shuffle the editorPicks array
|
||||
const shuffledPicks = shuffleArray([...editorPicks]);
|
||||
|
||||
const promises = shuffledPicks.map(async (pick) => {
|
||||
const endpoint =
|
||||
pick.type === "movie" ? `/movie/${pick.id}` : `/tv/${pick.id}`;
|
||||
const data = await get<any>(endpoint, {
|
||||
api_key: conf().TMDB_READ_API_KEY,
|
||||
language: "en-US",
|
||||
});
|
||||
return {
|
||||
...data,
|
||||
type: pick.type,
|
||||
};
|
||||
});
|
||||
const results = await Promise.all(promises);
|
||||
setEditorPicksData(results);
|
||||
} catch (error) {
|
||||
console.error("Error fetching editor picks:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEditorPicks();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMoviesForCategory = async (category: Category) => {
|
||||
try {
|
||||
|
|
@ -315,11 +373,13 @@ export function Discover() {
|
|||
const displayCategory =
|
||||
category === "Now Playing"
|
||||
? "In Cinemas"
|
||||
: category.includes("Movie")
|
||||
? `${category}s`
|
||||
: isTVShow
|
||||
? `${category} Shows`
|
||||
: `${category} Movies`;
|
||||
: category === "Editor Picks" // Check for "Editor Picks" specifically
|
||||
? category
|
||||
: category.includes("Movie")
|
||||
? `${category}s`
|
||||
: isTVShow
|
||||
? `${category} Shows`
|
||||
: `${category} Movies`;
|
||||
|
||||
// https://tailwindcss.com/docs/border-style
|
||||
return (
|
||||
|
|
@ -532,6 +592,16 @@ export function Discover() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor Picks Section */}
|
||||
<div className="mt-8">
|
||||
{editorPicksData.length > 0 && (
|
||||
<div className="mt-8">
|
||||
{renderMovies(editorPicksData, "Editor Picks")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{categories.map((category) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ export function HomePage() {
|
|||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-8">
|
||||
<BookmarksPart onItemsChange={setShowBookmarks} />
|
||||
<WatchingPart onItemsChange={setShowWatching} />
|
||||
<BookmarksPart onItemsChange={setShowBookmarks} />
|
||||
</div>
|
||||
{!(showBookmarks || showWatching) ? (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
|
|
@ -11,6 +11,8 @@ import { useBookmarkStore } from "@/stores/bookmarks";
|
|||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||
|
||||
export function BookmarksPart({
|
||||
onItemsChange,
|
||||
}: {
|
||||
|
|
@ -23,6 +25,8 @@ export function BookmarksPart({
|
|||
const [editing, setEditing] = useState(false);
|
||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const items = useMemo(() => {
|
||||
let output: MediaItem[] = [];
|
||||
Object.entries(bookmarks).forEach((entry) => {
|
||||
|
|
@ -49,15 +53,60 @@ export function BookmarksPart({
|
|||
onItemsChange(items.length > 0);
|
||||
}, [items, onItemsChange]);
|
||||
|
||||
const handleLongPress = () => {
|
||||
// Find the button by ID and simulate a click
|
||||
const editButton = document.getElementById("edit-button-bookmark");
|
||||
if (editButton) {
|
||||
(editButton as HTMLButtonElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Prevent default touch action
|
||||
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (pressTimerRef.current) {
|
||||
clearTimeout(pressTimerRef.current);
|
||||
pressTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Prevent default mouse action
|
||||
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (pressTimerRef.current) {
|
||||
clearTimeout(pressTimerRef.current);
|
||||
pressTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="relative"
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
} // Prevent right-click context menu
|
||||
onTouchStart={handleTouchStart} // Handle touch start
|
||||
onTouchEnd={handleTouchEnd} // Handle touch end
|
||||
onMouseDown={handleMouseDown} // Handle mouse down
|
||||
onMouseUp={handleMouseUp} // Handle mouse up
|
||||
>
|
||||
<SectionHeading
|
||||
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||
icon={Icons.BOOKMARK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id="edit-button-bookmark"
|
||||
/>
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{items.map((v) => (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
|
|
@ -7,25 +7,27 @@ import { Icons } from "@/components/Icon";
|
|||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { shouldShowProgress } from "@/stores/progress/utils";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||
|
||||
export function WatchingPart({
|
||||
onItemsChange,
|
||||
}: {
|
||||
onItemsChange: (hasItems: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
const progressItems = useProgressStore((s) => s.items);
|
||||
const removeItem = useProgressStore((s) => s.removeItem);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const sortedProgressItems = useMemo(() => {
|
||||
let output: MediaItem[] = [];
|
||||
const output: MediaItem[] = [];
|
||||
Object.entries(progressItems)
|
||||
.filter((entry) => shouldShowProgress(entry[1]).show)
|
||||
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
|
||||
|
|
@ -36,35 +38,79 @@ export function WatchingPart({
|
|||
});
|
||||
});
|
||||
|
||||
output = output.filter((v) => {
|
||||
const isBookMarked = !!bookmarks[v.id];
|
||||
return !isBookMarked;
|
||||
});
|
||||
return output;
|
||||
}, [progressItems, bookmarks]);
|
||||
}, [progressItems]);
|
||||
|
||||
useEffect(() => {
|
||||
onItemsChange(sortedProgressItems.length > 0);
|
||||
}, [sortedProgressItems, onItemsChange]);
|
||||
|
||||
const handleLongPress = () => {
|
||||
// Find the button by ID and simulate a click
|
||||
const editButton = document.getElementById("edit-button-watching");
|
||||
if (editButton) {
|
||||
(editButton as HTMLButtonElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Prevent default touch action
|
||||
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (pressTimerRef.current) {
|
||||
clearTimeout(pressTimerRef.current);
|
||||
pressTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Prevent default mouse action
|
||||
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (pressTimerRef.current) {
|
||||
clearTimeout(pressTimerRef.current);
|
||||
pressTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (sortedProgressItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="relative"
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
} // Prevent right-click context menu
|
||||
>
|
||||
<SectionHeading
|
||||
title={t("home.continueWatching.sectionTitle")}
|
||||
icon={Icons.CLOCK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id="edit-button-watching"
|
||||
/>
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{sortedProgressItems.map((v) => (
|
||||
<WatchedMediaCard
|
||||
key={v.id}
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeItem(v.id)}
|
||||
/>
|
||||
<div
|
||||
onTouchStart={handleTouchStart} // Handle touch start
|
||||
onTouchEnd={handleTouchEnd} // Handle touch end
|
||||
onMouseDown={handleMouseDown} // Handle mouse down
|
||||
onMouseUp={handleMouseUp} // Handle mouse up
|
||||
>
|
||||
<WatchedMediaCard
|
||||
key={v.id}
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeItem(v.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { ReactNode } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import { Player } from "@/components/player";
|
||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
|
|
@ -15,10 +18,17 @@ export interface PlayerPartProps {
|
|||
}
|
||||
|
||||
export function PlayerPart(props: PlayerPartProps) {
|
||||
const params = useParams<{
|
||||
media: string;
|
||||
episode?: string;
|
||||
season?: string;
|
||||
}>();
|
||||
const media = params.media;
|
||||
const { showTargets, showTouchTargets } = useShouldShowControls();
|
||||
const status = usePlayerStore((s) => s.status);
|
||||
const { isMobile } = useIsMobile();
|
||||
const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading);
|
||||
const { playerMeta: meta } = usePlayerMeta();
|
||||
|
||||
return (
|
||||
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||
|
|
@ -60,6 +70,30 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||
<Player.BackLink url={props.backUrl} />
|
||||
<span className="text mx-3 text-type-secondary">/</span>
|
||||
<Player.Title />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!media) return;
|
||||
const id = media
|
||||
.replace("tmdb-tv-", "")
|
||||
.replace("tmdb-movie-", "");
|
||||
let url;
|
||||
if (meta?.type === "movie") {
|
||||
url = `https://www.themoviedb.org/movie/${id}`;
|
||||
} else {
|
||||
url = `https://www.themoviedb.org/tv/${id}`;
|
||||
}
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="text-xs font-semibold text-type-secondary"
|
||||
icon={Icons.CIRCLE_QUESTION}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Player.BookmarkButton />
|
||||
</div>
|
||||
<div className="text-center hidden xl:flex justify-center items-center">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ const config: Config = {
|
|||
/* breakpoints */
|
||||
screens: {
|
||||
ssm: "400px",
|
||||
'2xl': '1921px', // Custom breakpoint for screens at least 1920px wide
|
||||
'3xl': '2650px', // Custom breakpoint for screens at least 2650px wide
|
||||
'4xl': '3840px', // Custom breakpoint for screens at least 4096px wide
|
||||
},
|
||||
|
||||
/* fonts */
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ export default defineConfig(({ mode }) => {
|
|||
name: "sudo-flix",
|
||||
short_name: "sudo-flix",
|
||||
description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)",
|
||||
theme_color: "#120f1d",
|
||||
background_color: "#120f1d",
|
||||
theme_color: "#000000",
|
||||
background_color: "#000000",
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue