diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts
index 859caf35..a2f23e2b 100644
--- a/src/components/player/display/base.ts
+++ b/src/components/player/display/base.ts
@@ -24,6 +24,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
let isFullscreen = false;
let isPausedBeforeSeeking = false;
let isSeeking = false;
+ let startAt = 0;
function setupSource(vid: HTMLVideoElement, src: LoadableSource) {
if (src.type === "hls") {
@@ -43,10 +44,12 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
hls.attachMedia(vid);
hls.loadSource(src.url);
+ vid.currentTime = startAt;
return;
}
vid.src = src.url;
+ vid.currentTime = startAt;
}
function setSource() {
@@ -108,10 +111,11 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
destroyVideoElement();
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
},
- load(newSource) {
+ load(newSource, startAtInput) {
if (!newSource) unloadSource();
source = newSource;
emit("loading", true);
+ startAt = startAtInput;
setSource();
},
diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts
index 5890d937..6ebdddb3 100644
--- a/src/components/player/display/displayInterface.ts
+++ b/src/components/player/display/displayInterface.ts
@@ -17,7 +17,7 @@ export type DisplayInterfaceEvents = {
export interface DisplayInterface extends Listener
{
play(): void;
pause(): void;
- load(source: LoadableSource | null): void;
+ load(source: LoadableSource | null, startAt: number): void;
processVideoElement(video: HTMLVideoElement): void;
processContainerElement(container: HTMLElement): void;
toggleFullscreen(): void;
diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts
index 13428a9b..6f7abad9 100644
--- a/src/components/player/hooks/usePlayer.ts
+++ b/src/components/player/hooks/usePlayer.ts
@@ -3,20 +3,38 @@ import { useInitializePlayer } from "@/components/player/hooks/useInitializePlay
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { SourceSliceSource } from "@/stores/player/utils/qualities";
+import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
export interface Source {
url: string;
type: MWStreamType;
}
+function getProgress(
+ items: Record,
+ meta: PlayerMeta | null
+): number {
+ const item = items[meta?.tmdbId ?? ""];
+ if (!item || !meta) return 0;
+ if (meta.type === "movie") {
+ if (!item.progress) return 0;
+ return item.progress.watched;
+ }
+
+ const ep = item.episodes[meta.episode?.tmdbId ?? ""];
+ if (!ep) return 0;
+ return ep.progress.watched;
+}
+
export function usePlayer() {
const setStatus = usePlayerStore((s) => s.setStatus);
const setMeta = usePlayerStore((s) => s.setMeta);
const setSource = usePlayerStore((s) => s.setSource);
const status = usePlayerStore((s) => s.status);
- const meta = usePlayerStore((s) => s.meta);
const reset = usePlayerStore((s) => s.reset);
+ const meta = usePlayerStore((s) => s.meta);
const { init } = useInitializePlayer();
+ const progressStore = useProgressStore();
return {
reset,
@@ -25,7 +43,7 @@ export function usePlayer() {
setMeta(m);
},
playMedia(source: SourceSliceSource) {
- setSource(source);
+ setSource(source, getProgress(progressStore.items, meta));
setStatus(playerStatus.PLAYING);
init();
},
diff --git a/src/components/player/internals/ContextUtils.tsx b/src/components/player/internals/ContextUtils.tsx
index d7a80653..71a7683b 100644
--- a/src/components/player/internals/ContextUtils.tsx
+++ b/src/components/player/internals/ContextUtils.tsx
@@ -119,7 +119,7 @@ function LinkChevron(props: { children?: React.ReactNode }) {
return (
{props.children}
-
+
);
}
diff --git a/src/components/player/internals/ProgressSaver.tsx b/src/components/player/internals/ProgressSaver.tsx
new file mode 100644
index 00000000..a8d85895
--- /dev/null
+++ b/src/components/player/internals/ProgressSaver.tsx
@@ -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;
+}
diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx
index 5ea537c3..9d8196a0 100644
--- a/src/pages/parts/player/PlayerPart.tsx
+++ b/src/pages/parts/player/PlayerPart.tsx
@@ -4,6 +4,7 @@ import { BrandPill } from "@/components/layout/BrandPill";
import { Player } from "@/components/player";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { PlayerMeta } from "@/stores/player/slices/source";
+import { usePlayerStore } from "@/stores/player/store";
export interface PlayerPartProps {
children?: ReactNode;
@@ -14,16 +15,19 @@ export interface PlayerPartProps {
export function PlayerPart(props: PlayerPartProps) {
const { showTargets, showTouchTargets } = useShouldShowControls();
+ const status = usePlayerStore((s) => s.status);
return (
{props.children}
-
-
-
-
+ {status === "playing" ? (
+
+
+
+
+ ) : null}
= (set, get) => ({
});
},
reset() {
- get().display?.load(null);
+ get().display?.load(null, 0);
set((s) => {
s.status = playerStatus.IDLE;
s.meta = null;
diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts
index 23857e02..8c6143a4 100644
--- a/src/stores/player/slices/source.ts
+++ b/src/stores/player/slices/source.ts
@@ -41,7 +41,7 @@ export interface SourceSlice {
currentQuality: SourceQuality | null;
meta: PlayerMeta | null;
setStatus(status: PlayerStatus): void;
- setSource(stream: SourceSliceSource): void;
+ setSource(stream: SourceSliceSource, startAt: number): void;
switchQuality(quality: SourceQuality): void;
setMeta(meta: PlayerMeta): void;
}
@@ -85,7 +85,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({
s.meta = meta;
});
},
- setSource(stream: SourceSliceSource) {
+ setSource(stream: SourceSliceSource, startAt: number) {
let qualities: string[] = [];
if (stream.type === "file") qualities = Object.keys(stream.qualities);
const store = get();
@@ -97,7 +97,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({
s.currentQuality = loadableStream.quality;
});
- store.display?.load(loadableStream.stream);
+ store.display?.load(loadableStream.stream, startAt);
},
switchQuality(quality) {
const store = get();
@@ -108,7 +108,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({
set((s) => {
s.currentQuality = quality;
});
- store.display?.load(selectedQuality);
+ store.display?.load(selectedQuality, store.progress.time);
}
},
});
diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts
new file mode 100644
index 00000000..b84be1ca
--- /dev/null
+++ b/src/stores/progress/index.ts
@@ -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;
+ episodes: Record;
+}
+
+export interface UpdateItemOptions {
+ meta: PlayerMeta;
+ progress: ProgressItem;
+}
+
+export interface ProgressStore {
+ items: Record;
+ updateItem(ops: UpdateItemOptions): void;
+}
+
+// TODO add migration from previous progress store
+export const useProgressStore = create(
+ persist(
+ immer((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",
+ }
+ )
+);
diff --git a/tailwind.config.js b/tailwind.config.js
index b3f15e19..68c60438 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -66,6 +66,12 @@ module.exports = {
light: "#2A2A71"
},
+ // Buttons
+ buttons: {
+ toggle: "#8D44D6",
+ toggleDisabled: "#202836"
+ },
+
// only used for body colors/textures
background: {
main: "#0A0A10",