mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
continue watching and progress bars
This commit is contained in:
parent
6353bf3799
commit
fb96026195
8 changed files with 100 additions and 33 deletions
|
@ -6,9 +6,21 @@ import { mediaTypeToJW } from "@/backend/metadata/justwatch";
|
|||
export interface MediaCardProps {
|
||||
media: MWMediaMeta;
|
||||
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 (
|
||||
<div
|
||||
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
|
||||
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={{
|
||||
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">
|
||||
<span>{media.title}</span>
|
||||
</h1>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { useWatchedContext } from "@/state/watched";
|
||||
import { useMemo } from "react";
|
||||
import { MediaCard } from "./MediaCard";
|
||||
|
||||
export interface WatchedMediaCardProps {
|
||||
|
@ -6,5 +8,17 @@ export interface 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 clickareaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// TODO fix infinite loop
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setMoved(true);
|
||||
if (timeout.current) clearTimeout(timeout.current);
|
||||
|
|
|
@ -21,6 +21,8 @@ export function ProgressControl() {
|
|||
ref,
|
||||
commitTime
|
||||
);
|
||||
|
||||
// TODO make dragging update timer
|
||||
useEffect(() => {
|
||||
if (dragRef.current === dragging) return;
|
||||
dragRef.current = dragging;
|
||||
|
|
|
@ -7,25 +7,7 @@ interface Props {
|
|||
onProgress?: (time: number, duration: number) => void;
|
||||
}
|
||||
|
||||
const FIVETEEN_MINUTES = 15 * 60;
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO fix infinite loops
|
||||
export function ProgressListenerControl(props: Props) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const didInitialize = useRef<true | null>(null);
|
||||
|
@ -50,14 +32,11 @@ export function ProgressListenerControl(props: Props) {
|
|||
useEffect(() => {
|
||||
if (didInitialize.current) return;
|
||||
if (!videoState.hasInitialized || Number.isNaN(videoState.duration)) return;
|
||||
if (
|
||||
props.startAt !== undefined &&
|
||||
shouldRestoreTime(props.startAt, videoState.duration)
|
||||
) {
|
||||
if (props.startAt !== undefined) {
|
||||
videoState.setTime(props.startAt);
|
||||
}
|
||||
didInitialize.current = true;
|
||||
}, [didInitialize, videoState, props]);
|
||||
}, [didInitialize, props, videoState]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MWStreamType } from "@/backend/helpers/streams";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
import { VideoPlayerDispatchContext } from "../VideoContext";
|
||||
|
||||
interface SourceControlProps {
|
||||
|
@ -9,13 +9,16 @@ interface SourceControlProps {
|
|||
|
||||
export function SourceControl(props: SourceControlProps) {
|
||||
const dispatch = useContext(VideoPlayerDispatchContext);
|
||||
const didInitialize = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (didInitialize.current) return;
|
||||
dispatch({
|
||||
type: "SET_SOURCE",
|
||||
url: props.source,
|
||||
sourceType: props.type,
|
||||
});
|
||||
didInitialize.current = true;
|
||||
}, [props, dispatch]);
|
||||
|
||||
return null;
|
||||
|
|
|
@ -21,6 +21,7 @@ if (key) {
|
|||
// - mobile UI
|
||||
// - season/episode select
|
||||
// - chrome cast support
|
||||
// - airplay support
|
||||
// - source selection
|
||||
// - safari fullscreen will make video overlap player controls
|
||||
// - safari progress bar is fucked (video doesnt change time but video.currentTime does change)
|
||||
|
@ -47,7 +48,6 @@ if (key) {
|
|||
// - localize everything
|
||||
// - add titles to pages
|
||||
// - find place for bookmark button
|
||||
// - find place for progress bar for "continue watching" section
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
|
|
@ -10,6 +10,25 @@ import {
|
|||
} from "react";
|
||||
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 {
|
||||
meta: MWMediaMeta;
|
||||
series?: {
|
||||
|
@ -66,8 +85,12 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||
const contextValue = useMemo(
|
||||
() => ({
|
||||
updateProgress(media: MediaItem, progress: number, total: number): void {
|
||||
// TODO series support
|
||||
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) {
|
||||
item = {
|
||||
item: {
|
||||
|
@ -78,12 +101,20 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||
progress: 0,
|
||||
percentage: 0,
|
||||
};
|
||||
data.items.push(item);
|
||||
newData.items.push(item);
|
||||
}
|
||||
// update actual item
|
||||
item.progress = progress;
|
||||
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() {
|
||||
|
|
Loading…
Reference in a new issue