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="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" />
|
||||||
<meta name="msapplication-TileColor" content="#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-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
|
||||||
<link rel="apple-touch-startup-image"
|
<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)"
|
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">
|
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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
font-family: "Lato", sans-serif !important;
|
||||||
@apply bg-background-main font-main text-type-text;
|
@apply bg-background-main font-main text-type-text;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
|
@ -79,6 +81,16 @@ html[data-no-scroll], html[data-no-scroll] body {
|
||||||
min-height: 100dvh;
|
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)
|
/*generated with Input range slider CSS style generator (version 20211225)
|
||||||
https://toughengineer.github.io/demo/slider-styler*/
|
https://toughengineer.github.io/demo/slider-styler*/
|
||||||
:root {
|
:root {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
@ -7,31 +7,48 @@ import { Icon, Icons } from "@/components/Icon";
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
onEdit?: (editing: boolean) => void;
|
onEdit?: (editing: boolean) => void;
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditButton(props: EditButtonProps) {
|
export function EditButton(props: EditButtonProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
props.onEdit?.(!props.editing);
|
props.onEdit?.(!props.editing);
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<button
|
||||||
onClick={onClick}
|
ref={buttonRef}
|
||||||
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"
|
type="button"
|
||||||
>
|
onClick={onClick}
|
||||||
<span ref={parent}>
|
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"
|
||||||
{props.editing ? (
|
id={props.id} // Assign id to the button
|
||||||
<span className="mx-2 sm:mx-4 whitespace-nowrap">
|
>
|
||||||
{t("home.mediaList.stopEditing")}
|
<span ref={parent}>
|
||||||
</span>
|
{props.editing ? (
|
||||||
) : (
|
<span className="mx-2 sm:mx-4 whitespace-nowrap">
|
||||||
<Icon icon={Icons.EDIT} />
|
{t("home.mediaList.stopEditing")}
|
||||||
)}
|
</span>
|
||||||
</span>
|
) : (
|
||||||
</button>
|
<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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mx-auto max-w-full px-8 ${
|
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.classNames || ""}`}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
import { MediaBookmarkButton } from "./MediaBookmark";
|
import { MediaBookmarkButton } from "./MediaBookmark";
|
||||||
import { IconPatch } from "../buttons/IconPatch";
|
import { IconPatch } from "../buttons/IconPatch";
|
||||||
import { Icons } from "../Icon";
|
import { Icon, Icons } from "../Icon";
|
||||||
|
|
||||||
export interface MediaCardProps {
|
export interface MediaCardProps {
|
||||||
media: MediaItem;
|
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">
|
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
|
||||||
<span>{media.title}</span>
|
<span>{media.title}</span>
|
||||||
</h1>
|
</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.Child>
|
||||||
</Flare.Base>
|
</Flare.Base>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const MediaGrid = forwardRef<HTMLDivElement, MediaGridProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,25 @@ import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
import { Icon, Icons } from "../components/Icon";
|
import { Icon, Icons } from "../components/Icon";
|
||||||
import { PageTitle } from "./parts/util/PageTitle";
|
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() {
|
export function Discover() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [genres, setGenres] = useState<Genre[]>([]);
|
const [genres, setGenres] = useState<Genre[]>([]);
|
||||||
|
|
@ -48,6 +67,45 @@ export function Discover() {
|
||||||
const [countdownTimeout, setCountdownTimeout] =
|
const [countdownTimeout, setCountdownTimeout] =
|
||||||
useState<NodeJS.Timeout | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchMoviesForCategory = async (category: Category) => {
|
const fetchMoviesForCategory = async (category: Category) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -315,11 +373,13 @@ export function Discover() {
|
||||||
const displayCategory =
|
const displayCategory =
|
||||||
category === "Now Playing"
|
category === "Now Playing"
|
||||||
? "In Cinemas"
|
? "In Cinemas"
|
||||||
: category.includes("Movie")
|
: category === "Editor Picks" // Check for "Editor Picks" specifically
|
||||||
? `${category}s`
|
? category
|
||||||
: isTVShow
|
: category.includes("Movie")
|
||||||
? `${category} Shows`
|
? `${category}s`
|
||||||
: `${category} Movies`;
|
: isTVShow
|
||||||
|
? `${category} Shows`
|
||||||
|
: `${category} Movies`;
|
||||||
|
|
||||||
// https://tailwindcss.com/docs/border-style
|
// https://tailwindcss.com/docs/border-style
|
||||||
return (
|
return (
|
||||||
|
|
@ -532,6 +592,16 @@ export function Discover() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ export function HomePage() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<BookmarksPart onItemsChange={setShowBookmarks} />
|
|
||||||
<WatchingPart onItemsChange={setShowWatching} />
|
<WatchingPart onItemsChange={setShowWatching} />
|
||||||
|
<BookmarksPart onItemsChange={setShowBookmarks} />
|
||||||
</div>
|
</div>
|
||||||
{!(showBookmarks || showWatching) ? (
|
{!(showBookmarks || showWatching) ? (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { EditButton } from "@/components/buttons/EditButton";
|
import { EditButton } from "@/components/buttons/EditButton";
|
||||||
|
|
@ -11,6 +11,8 @@ import { useBookmarkStore } from "@/stores/bookmarks";
|
||||||
import { useProgressStore } from "@/stores/progress";
|
import { useProgressStore } from "@/stores/progress";
|
||||||
import { MediaItem } from "@/utils/mediaTypes";
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||||
|
|
||||||
export function BookmarksPart({
|
export function BookmarksPart({
|
||||||
onItemsChange,
|
onItemsChange,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -23,6 +25,8 @@ export function BookmarksPart({
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
|
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
let output: MediaItem[] = [];
|
let output: MediaItem[] = [];
|
||||||
Object.entries(bookmarks).forEach((entry) => {
|
Object.entries(bookmarks).forEach((entry) => {
|
||||||
|
|
@ -49,15 +53,60 @@ export function BookmarksPart({
|
||||||
onItemsChange(items.length > 0);
|
onItemsChange(items.length > 0);
|
||||||
}, [items, onItemsChange]);
|
}, [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;
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
return (
|
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
|
<SectionHeading
|
||||||
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||||
icon={Icons.BOOKMARK}
|
icon={Icons.BOOKMARK}
|
||||||
>
|
>
|
||||||
<EditButton editing={editing} onEdit={setEditing} />
|
<EditButton
|
||||||
|
editing={editing}
|
||||||
|
onEdit={setEditing}
|
||||||
|
id="edit-button-bookmark"
|
||||||
|
/>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
<MediaGrid ref={gridRef}>
|
<MediaGrid ref={gridRef}>
|
||||||
{items.map((v) => (
|
{items.map((v) => (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { EditButton } from "@/components/buttons/EditButton";
|
import { EditButton } from "@/components/buttons/EditButton";
|
||||||
|
|
@ -7,25 +7,27 @@ import { Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
|
||||||
import { useProgressStore } from "@/stores/progress";
|
import { useProgressStore } from "@/stores/progress";
|
||||||
import { shouldShowProgress } from "@/stores/progress/utils";
|
import { shouldShowProgress } from "@/stores/progress/utils";
|
||||||
import { MediaItem } from "@/utils/mediaTypes";
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||||
|
|
||||||
export function WatchingPart({
|
export function WatchingPart({
|
||||||
onItemsChange,
|
onItemsChange,
|
||||||
}: {
|
}: {
|
||||||
onItemsChange: (hasItems: boolean) => void;
|
onItemsChange: (hasItems: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
|
||||||
const progressItems = useProgressStore((s) => s.items);
|
const progressItems = useProgressStore((s) => s.items);
|
||||||
const removeItem = useProgressStore((s) => s.removeItem);
|
const removeItem = useProgressStore((s) => s.removeItem);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
|
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const sortedProgressItems = useMemo(() => {
|
const sortedProgressItems = useMemo(() => {
|
||||||
let output: MediaItem[] = [];
|
const output: MediaItem[] = [];
|
||||||
Object.entries(progressItems)
|
Object.entries(progressItems)
|
||||||
.filter((entry) => shouldShowProgress(entry[1]).show)
|
.filter((entry) => shouldShowProgress(entry[1]).show)
|
||||||
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
|
.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;
|
return output;
|
||||||
}, [progressItems, bookmarks]);
|
}, [progressItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onItemsChange(sortedProgressItems.length > 0);
|
onItemsChange(sortedProgressItems.length > 0);
|
||||||
}, [sortedProgressItems, onItemsChange]);
|
}, [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;
|
if (sortedProgressItems.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className="relative"
|
||||||
|
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
e.preventDefault()
|
||||||
|
} // Prevent right-click context menu
|
||||||
|
>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title={t("home.continueWatching.sectionTitle")}
|
title={t("home.continueWatching.sectionTitle")}
|
||||||
icon={Icons.CLOCK}
|
icon={Icons.CLOCK}
|
||||||
>
|
>
|
||||||
<EditButton editing={editing} onEdit={setEditing} />
|
<EditButton
|
||||||
|
editing={editing}
|
||||||
|
onEdit={setEditing}
|
||||||
|
id="edit-button-watching"
|
||||||
|
/>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
<MediaGrid ref={gridRef}>
|
<MediaGrid ref={gridRef}>
|
||||||
{sortedProgressItems.map((v) => (
|
{sortedProgressItems.map((v) => (
|
||||||
<WatchedMediaCard
|
<div
|
||||||
key={v.id}
|
onTouchStart={handleTouchStart} // Handle touch start
|
||||||
media={v}
|
onTouchEnd={handleTouchEnd} // Handle touch end
|
||||||
closable={editing}
|
onMouseDown={handleMouseDown} // Handle mouse down
|
||||||
onClose={() => removeItem(v.id)}
|
onMouseUp={handleMouseUp} // Handle mouse up
|
||||||
/>
|
>
|
||||||
|
<WatchedMediaCard
|
||||||
|
key={v.id}
|
||||||
|
media={v}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeItem(v.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</MediaGrid>
|
</MediaGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { BrandPill } from "@/components/layout/BrandPill";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
|
|
@ -15,10 +18,17 @@ export interface PlayerPartProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlayerPart(props: PlayerPartProps) {
|
export function PlayerPart(props: PlayerPartProps) {
|
||||||
|
const params = useParams<{
|
||||||
|
media: string;
|
||||||
|
episode?: string;
|
||||||
|
season?: string;
|
||||||
|
}>();
|
||||||
|
const media = params.media;
|
||||||
const { showTargets, showTouchTargets } = useShouldShowControls();
|
const { showTargets, showTouchTargets } = useShouldShowControls();
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading);
|
const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading);
|
||||||
|
const { playerMeta: meta } = usePlayerMeta();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||||
|
|
@ -60,6 +70,30 @@ export function PlayerPart(props: PlayerPartProps) {
|
||||||
<Player.BackLink url={props.backUrl} />
|
<Player.BackLink url={props.backUrl} />
|
||||||
<span className="text mx-3 text-type-secondary">/</span>
|
<span className="text mx-3 text-type-secondary">/</span>
|
||||||
<Player.Title />
|
<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 />
|
<Player.BookmarkButton />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center hidden xl:flex justify-center items-center">
|
<div className="text-center hidden xl:flex justify-center items-center">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ const config: Config = {
|
||||||
/* breakpoints */
|
/* breakpoints */
|
||||||
screens: {
|
screens: {
|
||||||
ssm: "400px",
|
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 */
|
/* fonts */
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ export default defineConfig(({ mode }) => {
|
||||||
name: "sudo-flix",
|
name: "sudo-flix",
|
||||||
short_name: "sudo-flix",
|
short_name: "sudo-flix",
|
||||||
description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)",
|
description: "Watch your favorite shows and movies for free with no ads ever! (っ'ヮ'c)",
|
||||||
theme_color: "#120f1d",
|
theme_color: "#000000",
|
||||||
background_color: "#120f1d",
|
background_color: "#000000",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
icons: [
|
icons: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue