13 changed files with 254 additions and 18 deletions
@ -0,0 +1,23 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
|
||||||
|
export function Toggle(props: { onClick: () => void; enabled?: boolean }) { |
||||||
|
return ( |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
onClick={props.onClick} |
||||||
|
className={classNames( |
||||||
|
"w-11 h-6 p-1 rounded-full grid transition-colors duration-100 group/toggle", |
||||||
|
props.enabled ? "bg-buttons-toggle" : "bg-buttons-toggleDisabled" |
||||||
|
)} |
||||||
|
> |
||||||
|
<div className="relative w-full h-full"> |
||||||
|
<div |
||||||
|
className={classNames( |
||||||
|
"scale-90 group-hover/toggle:scale-100 h-full aspect-square rounded-full bg-white absolute transition-all duration-100", |
||||||
|
props.enabled ? "left-full transform -translate-x-full" : "left-0" |
||||||
|
)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
import { useInterval } from "react-use"; |
||||||
|
|
||||||
|
import { usePlayerStore } from "@/stores/player/store"; |
||||||
|
import { useProgressStore } from "@/stores/progress"; |
||||||
|
|
||||||
|
export function ProgressSaver() { |
||||||
|
const meta = usePlayerStore((s) => s.meta); |
||||||
|
const progress = usePlayerStore((s) => s.progress); |
||||||
|
const updateItem = useProgressStore((s) => s.updateItem); |
||||||
|
|
||||||
|
const updateItemRef = useRef(updateItem); |
||||||
|
useEffect(() => { |
||||||
|
updateItemRef.current = updateItem; |
||||||
|
}, [updateItem]); |
||||||
|
|
||||||
|
const metaRef = useRef(meta); |
||||||
|
useEffect(() => { |
||||||
|
metaRef.current = meta; |
||||||
|
}, [meta]); |
||||||
|
|
||||||
|
const progressRef = useRef(progress); |
||||||
|
useEffect(() => { |
||||||
|
progressRef.current = progress; |
||||||
|
}, [progress]); |
||||||
|
|
||||||
|
useInterval(() => { |
||||||
|
if (updateItemRef.current && metaRef.current && progressRef.current) |
||||||
|
updateItemRef.current({ |
||||||
|
meta: metaRef.current, |
||||||
|
progress: { |
||||||
|
duration: progress.duration, |
||||||
|
watched: progress.time, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, 3000); |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
import { create } from "zustand"; |
||||||
|
import { persist } from "zustand/middleware"; |
||||||
|
import { immer } from "zustand/middleware/immer"; |
||||||
|
|
||||||
|
import { PlayerMeta } from "@/stores/player/slices/source"; |
||||||
|
|
||||||
|
export interface ProgressItem { |
||||||
|
watched: number; |
||||||
|
duration: number; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProgressSeasonItem { |
||||||
|
title: string; |
||||||
|
number: number; |
||||||
|
id: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProgressEpisodeItem { |
||||||
|
title: string; |
||||||
|
number: number; |
||||||
|
id: string; |
||||||
|
seasonId: string; |
||||||
|
progress: ProgressItem; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProgressMediaItem { |
||||||
|
title: string; |
||||||
|
year: number; |
||||||
|
type: "show" | "movie"; |
||||||
|
progress?: ProgressItem; |
||||||
|
seasons: Record<string, ProgressSeasonItem>; |
||||||
|
episodes: Record<string, ProgressEpisodeItem>; |
||||||
|
} |
||||||
|
|
||||||
|
export interface UpdateItemOptions { |
||||||
|
meta: PlayerMeta; |
||||||
|
progress: ProgressItem; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProgressStore { |
||||||
|
items: Record<string, ProgressMediaItem>; |
||||||
|
updateItem(ops: UpdateItemOptions): void; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO add migration from previous progress store
|
||||||
|
export const useProgressStore = create( |
||||||
|
persist( |
||||||
|
immer<ProgressStore>((set) => ({ |
||||||
|
items: {}, |
||||||
|
updateItem({ meta, progress }) { |
||||||
|
set((s) => { |
||||||
|
if (!s.items[meta.tmdbId]) |
||||||
|
s.items[meta.tmdbId] = { |
||||||
|
type: meta.type, |
||||||
|
episodes: {}, |
||||||
|
seasons: {}, |
||||||
|
title: meta.title, |
||||||
|
year: meta.releaseYear, |
||||||
|
}; |
||||||
|
const item = s.items[meta.tmdbId]; |
||||||
|
if (meta.type === "movie") { |
||||||
|
if (!item.progress) |
||||||
|
item.progress = { |
||||||
|
duration: 0, |
||||||
|
watched: 0, |
||||||
|
}; |
||||||
|
item.progress = { ...progress }; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!meta.episode || !meta.season) return; |
||||||
|
|
||||||
|
if (!item.seasons[meta.season.tmdbId]) |
||||||
|
item.seasons[meta.season.tmdbId] = { |
||||||
|
id: meta.season.tmdbId, |
||||||
|
number: meta.season.number, |
||||||
|
title: meta.season.title, |
||||||
|
}; |
||||||
|
|
||||||
|
if (!item.episodes[meta.episode.tmdbId]) |
||||||
|
item.episodes[meta.episode.tmdbId] = { |
||||||
|
id: meta.episode.tmdbId, |
||||||
|
number: meta.episode.number, |
||||||
|
title: meta.episode.title, |
||||||
|
seasonId: meta.season.tmdbId, |
||||||
|
progress: { |
||||||
|
duration: 0, |
||||||
|
watched: 0, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
item.episodes[meta.episode.tmdbId].progress = { ...progress }; |
||||||
|
}); |
||||||
|
}, |
||||||
|
})), |
||||||
|
{ |
||||||
|
name: "__MW::progress", |
||||||
|
} |
||||||
|
) |
||||||
|
); |
Loading…
Reference in new issue