diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx
index e51db836..1b23b974 100644
--- a/src/components/media/MediaCard.tsx
+++ b/src/components/media/MediaCard.tsx
@@ -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 (
+ >
+ {series ? (
+
+
+ S{series.season} E{series.episode}
+
+
+ ) : null}
+
+ {percentage !== undefined ? (
+ <>
+
+
+
+ >
+ ) : null}
+
{media.title}
diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx
index 6dcf7064..1dd11d5e 100644
--- a/src/components/media/WatchedMediaCard.tsx
+++ b/src/components/media/WatchedMediaCard.tsx
@@ -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 ;
+ const { watched } = useWatchedContext();
+ const watchedMedia = useMemo(() => {
+ return watched.items.find((v) => v.item.meta.id === props.media.id);
+ }, [watched, props.media]);
+
+ return (
+
+ );
}
diff --git a/src/components/video/controls/BackdropControl.tsx b/src/components/video/controls/BackdropControl.tsx
index 4c081ba7..2d099627 100644
--- a/src/components/video/controls/BackdropControl.tsx
+++ b/src/components/video/controls/BackdropControl.tsx
@@ -12,6 +12,7 @@ export function BackdropControl(props: BackdropControlProps) {
const timeout = useRef | null>(null);
const clickareaRef = useRef(null);
+ // TODO fix infinite loop
const handleMouseMove = useCallback(() => {
setMoved(true);
if (timeout.current) clearTimeout(timeout.current);
diff --git a/src/components/video/controls/ProgressControl.tsx b/src/components/video/controls/ProgressControl.tsx
index b8e277a0..eaeed9ee 100644
--- a/src/components/video/controls/ProgressControl.tsx
+++ b/src/components/video/controls/ProgressControl.tsx
@@ -21,6 +21,8 @@ export function ProgressControl() {
ref,
commitTime
);
+
+ // TODO make dragging update timer
useEffect(() => {
if (dragRef.current === dragging) return;
dragRef.current = dragging;
diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/components/video/controls/ProgressListenerControl.tsx
index a8f9a80d..b20fc4c8 100644
--- a/src/components/video/controls/ProgressListenerControl.tsx
+++ b/src/components/video/controls/ProgressListenerControl.tsx
@@ -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(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;
}
diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx
index 0e612a50..9025c404 100644
--- a/src/components/video/controls/SourceControl.tsx
+++ b/src/components/video/controls/SourceControl.tsx
@@ -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;
diff --git a/src/index.tsx b/src/index.tsx
index f2f33418..5d345107 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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(
diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx
index cefec243..dd44ba58 100644
--- a/src/state/watched/context.tsx
+++ b/src/state/watched/context.tsx
@@ -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() {