mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 06:31:48 +01:00
continue watching and progress bars
This commit is contained in:
parent
6353bf3799
commit
fb96026195
@ -6,9 +6,21 @@ import { mediaTypeToJW } from "@/backend/metadata/justwatch";
|
|||||||
export interface MediaCardProps {
|
export interface MediaCardProps {
|
||||||
media: MWMediaMeta;
|
media: MWMediaMeta;
|
||||||
linkable?: boolean;
|
linkable?: boolean;
|
||||||
|
series?: {
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
};
|
||||||
|
percentage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MediaCardContent({ media, linkable }: MediaCardProps) {
|
function MediaCardContent({
|
||||||
|
media,
|
||||||
|
linkable,
|
||||||
|
series,
|
||||||
|
percentage,
|
||||||
|
}: MediaCardProps) {
|
||||||
|
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
||||||
@ -21,11 +33,36 @@ function MediaCardContent({ media, linkable }: MediaCardProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mb-4 aspect-[2/3] w-full rounded-xl bg-denim-500 bg-cover"
|
className="relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
|
backgroundImage: media.poster ? `url(${media.poster})` : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{series ? (
|
||||||
|
<div className="absolute right-2 top-2 rounded-md bg-denim-200 py-1 px-2 transition-colors group-hover:bg-denim-500">
|
||||||
|
<p className="text-center text-xs font-bold text-slate-400 transition-colors group-hover:text-white">
|
||||||
|
S{series.season} E{series.episode}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{percentage !== undefined ? (
|
||||||
|
<>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors group-hover:from-denim-100" />
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors group-hover:from-denim-100" />
|
||||||
|
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||||
|
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 rounded-full bg-bink-700"
|
||||||
|
style={{
|
||||||
|
width: percentageString,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
|
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
|
||||||
<span>{media.title}</span>
|
<span>{media.title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
|
import { useWatchedContext } from "@/state/watched";
|
||||||
|
import { useMemo } from "react";
|
||||||
import { MediaCard } from "./MediaCard";
|
import { MediaCard } from "./MediaCard";
|
||||||
|
|
||||||
export interface WatchedMediaCardProps {
|
export interface WatchedMediaCardProps {
|
||||||
@ -6,5 +8,17 @@ export interface WatchedMediaCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
||||||
return <MediaCard media={props.media} linkable />;
|
const { watched } = useWatchedContext();
|
||||||
|
const watchedMedia = useMemo(() => {
|
||||||
|
return watched.items.find((v) => v.item.meta.id === props.media.id);
|
||||||
|
}, [watched, props.media]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MediaCard
|
||||||
|
media={props.media}
|
||||||
|
series={watchedMedia?.item?.series}
|
||||||
|
linkable
|
||||||
|
percentage={watchedMedia?.percentage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ export function BackdropControl(props: BackdropControlProps) {
|
|||||||
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const clickareaRef = useRef<HTMLDivElement>(null);
|
const clickareaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// TODO fix infinite loop
|
||||||
const handleMouseMove = useCallback(() => {
|
const handleMouseMove = useCallback(() => {
|
||||||
setMoved(true);
|
setMoved(true);
|
||||||
if (timeout.current) clearTimeout(timeout.current);
|
if (timeout.current) clearTimeout(timeout.current);
|
||||||
|
@ -21,6 +21,8 @@ export function ProgressControl() {
|
|||||||
ref,
|
ref,
|
||||||
commitTime
|
commitTime
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO make dragging update timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dragRef.current === dragging) return;
|
if (dragRef.current === dragging) return;
|
||||||
dragRef.current = dragging;
|
dragRef.current = dragging;
|
||||||
|
@ -7,25 +7,7 @@ interface Props {
|
|||||||
onProgress?: (time: number, duration: number) => void;
|
onProgress?: (time: number, duration: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIVETEEN_MINUTES = 15 * 60;
|
// TODO fix infinite loops
|
||||||
const FIVE_MINUTES = 5 * 60;
|
|
||||||
|
|
||||||
function shouldRestoreTime(time: number, duration: number): boolean {
|
|
||||||
const timeFromEnd = Math.max(0, duration - time);
|
|
||||||
|
|
||||||
// short movie
|
|
||||||
if (duration < FIVETEEN_MINUTES) {
|
|
||||||
if (time < 5) return false;
|
|
||||||
if (timeFromEnd < 60) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// long movie
|
|
||||||
if (time < 30) return false;
|
|
||||||
if (timeFromEnd < FIVE_MINUTES) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProgressListenerControl(props: Props) {
|
export function ProgressListenerControl(props: Props) {
|
||||||
const { videoState } = useVideoPlayerState();
|
const { videoState } = useVideoPlayerState();
|
||||||
const didInitialize = useRef<true | null>(null);
|
const didInitialize = useRef<true | null>(null);
|
||||||
@ -50,14 +32,11 @@ export function ProgressListenerControl(props: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didInitialize.current) return;
|
if (didInitialize.current) return;
|
||||||
if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return;
|
if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return;
|
||||||
if (
|
if (props.startAt !== undefined) {
|
||||||
props.startAt !== undefined &&
|
|
||||||
shouldRestoreTime(props.startAt, videoState.duration)
|
|
||||||
) {
|
|
||||||
videoState.setTime(props.startAt);
|
videoState.setTime(props.startAt);
|
||||||
}
|
}
|
||||||
didInitialize.current = true;
|
didInitialize.current = true;
|
||||||
}, [didInitialize, videoState, props]);
|
}, [didInitialize, props, videoState]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import { VideoPlayerDispatchContext } from "../VideoContext";
|
import { VideoPlayerDispatchContext } from "../VideoContext";
|
||||||
|
|
||||||
interface SourceControlProps {
|
interface SourceControlProps {
|
||||||
@ -9,13 +9,16 @@ interface SourceControlProps {
|
|||||||
|
|
||||||
export function SourceControl(props: SourceControlProps) {
|
export function SourceControl(props: SourceControlProps) {
|
||||||
const dispatch = useContext(VideoPlayerDispatchContext);
|
const dispatch = useContext(VideoPlayerDispatchContext);
|
||||||
|
const didInitialize = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (didInitialize.current) return;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_SOURCE",
|
type: "SET_SOURCE",
|
||||||
url: props.source,
|
url: props.source,
|
||||||
sourceType: props.type,
|
sourceType: props.type,
|
||||||
});
|
});
|
||||||
|
didInitialize.current = true;
|
||||||
}, [props, dispatch]);
|
}, [props, dispatch]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -21,6 +21,7 @@ if (key) {
|
|||||||
// - mobile UI
|
// - mobile UI
|
||||||
// - season/episode select
|
// - season/episode select
|
||||||
// - chrome cast support
|
// - chrome cast support
|
||||||
|
// - airplay support
|
||||||
// - source selection
|
// - source selection
|
||||||
// - safari fullscreen will make video overlap player controls
|
// - safari fullscreen will make video overlap player controls
|
||||||
// - safari progress bar is fucked (video doesnt change time but video.currentTime does change)
|
// - safari progress bar is fucked (video doesnt change time but video.currentTime does change)
|
||||||
@ -47,7 +48,6 @@ if (key) {
|
|||||||
// - localize everything
|
// - localize everything
|
||||||
// - add titles to pages
|
// - add titles to pages
|
||||||
// - find place for bookmark button
|
// - find place for bookmark button
|
||||||
// - find place for progress bar for "continue watching" section
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
@ -10,6 +10,25 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { VideoProgressStore } from "./store";
|
import { VideoProgressStore } from "./store";
|
||||||
|
|
||||||
|
const FIVETEEN_MINUTES = 15 * 60;
|
||||||
|
const FIVE_MINUTES = 5 * 60;
|
||||||
|
|
||||||
|
function shouldSave(time: number, duration: number): boolean {
|
||||||
|
const timeFromEnd = Math.max(0, duration - time);
|
||||||
|
|
||||||
|
// short movie
|
||||||
|
if (duration < FIVETEEN_MINUTES) {
|
||||||
|
if (time < 5) return false;
|
||||||
|
if (timeFromEnd < 60) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// long movie
|
||||||
|
if (time < 30) return false;
|
||||||
|
if (timeFromEnd < FIVE_MINUTES) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
meta: MWMediaMeta;
|
meta: MWMediaMeta;
|
||||||
series?: {
|
series?: {
|
||||||
@ -66,8 +85,12 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
updateProgress(media: MediaItem, progress: number, total: number): void {
|
updateProgress(media: MediaItem, progress: number, total: number): void {
|
||||||
|
// TODO series support
|
||||||
setWatched((data: WatchedStoreData) => {
|
setWatched((data: WatchedStoreData) => {
|
||||||
let item = data.items.find((v) => v.item.meta.id === media.meta.id);
|
const newData = { ...data };
|
||||||
|
let item = newData.items.find(
|
||||||
|
(v) => v.item.meta.id === media.meta.id
|
||||||
|
);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
item = {
|
item = {
|
||||||
item: {
|
item: {
|
||||||
@ -78,12 +101,20 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
};
|
};
|
||||||
data.items.push(item);
|
newData.items.push(item);
|
||||||
}
|
}
|
||||||
// update actual item
|
// update actual item
|
||||||
item.progress = progress;
|
item.progress = progress;
|
||||||
item.percentage = Math.round((progress / total) * 100);
|
item.percentage = Math.round((progress / total) * 100);
|
||||||
return data;
|
|
||||||
|
// remove item if shouldnt save
|
||||||
|
if (!shouldSave(progress, total)) {
|
||||||
|
newData.items = data.items.filter(
|
||||||
|
(v) => v.item.meta.id !== media.meta.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newData;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFilteredWatched() {
|
getFilteredWatched() {
|
||||||
|
Loading…
Reference in New Issue
Block a user