mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 23:41:49 +01:00
progress saving, progress restoring, toggle in caption settings
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
f8ec45bf13
commit
accc13ab0e
23
src/components/buttons/Toggle.tsx
Normal file
23
src/components/buttons/Toggle.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
|
import { OverlayAnchor } from "@/components/overlays/OverlayAnchor";
|
||||||
import { Overlay } from "@/components/overlays/OverlayDisplay";
|
import { Overlay } from "@/components/overlays/OverlayDisplay";
|
||||||
@ -92,10 +93,31 @@ function QualityView({ id }: { id: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CaptionSettingsView({ id }: { id: string }) {
|
||||||
|
const router = useOverlayRouter(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Context.BackLink onClick={() => router.navigate("/captions")}>
|
||||||
|
Custom captions
|
||||||
|
</Context.BackLink>
|
||||||
|
<Context.Section>
|
||||||
|
<Context.SmallText>Hello!</Context.SmallText>
|
||||||
|
</Context.Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SettingsOverlay({ id }: { id: string }) {
|
function SettingsOverlay({ id }: { id: string }) {
|
||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||||
|
|
||||||
|
const [tmpBool, setTmpBool] = useState(false);
|
||||||
|
|
||||||
|
function toggleBool() {
|
||||||
|
setTmpBool(!tmpBool);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay id={id}>
|
<Overlay id={id}>
|
||||||
<OverlayRouter id={id}>
|
<OverlayRouter id={id}>
|
||||||
@ -121,11 +143,11 @@ function SettingsOverlay({ id }: { id: string }) {
|
|||||||
|
|
||||||
<Context.SectionTitle>Viewing Experience</Context.SectionTitle>
|
<Context.SectionTitle>Viewing Experience</Context.SectionTitle>
|
||||||
<Context.Section>
|
<Context.Section>
|
||||||
<Context.Link onClick={() => router.navigate("/quality")}>
|
|
||||||
<Context.LinkTitle>Enable Captions</Context.LinkTitle>
|
|
||||||
<Context.LinkChevron />
|
|
||||||
</Context.Link>
|
|
||||||
<Context.Link>
|
<Context.Link>
|
||||||
|
<Context.LinkTitle>Enable Captions</Context.LinkTitle>
|
||||||
|
<Toggle enabled={tmpBool} onClick={() => toggleBool()} />
|
||||||
|
</Context.Link>
|
||||||
|
<Context.Link onClick={() => router.navigate("/captions")}>
|
||||||
<Context.LinkTitle>Caption settings</Context.LinkTitle>
|
<Context.LinkTitle>Caption settings</Context.LinkTitle>
|
||||||
<Context.LinkChevron>English</Context.LinkChevron>
|
<Context.LinkChevron>English</Context.LinkChevron>
|
||||||
</Context.Link>
|
</Context.Link>
|
||||||
@ -141,6 +163,24 @@ function SettingsOverlay({ id }: { id: string }) {
|
|||||||
<QualityView id={id} />
|
<QualityView id={id} />
|
||||||
</Context.Card>
|
</Context.Card>
|
||||||
</OverlayPage>
|
</OverlayPage>
|
||||||
|
<OverlayPage id={id} path="/captions" width={343} height={431}>
|
||||||
|
<Context.Card>
|
||||||
|
<Context.BackLink onClick={() => router.navigate("/")}>
|
||||||
|
Captions
|
||||||
|
</Context.BackLink>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.navigate("/captions/settings")}
|
||||||
|
>
|
||||||
|
Go to caption settings
|
||||||
|
</button>
|
||||||
|
</Context.Card>
|
||||||
|
</OverlayPage>
|
||||||
|
<OverlayPage id={id} path="/captions/settings" width={343} height={431}>
|
||||||
|
<Context.Card>
|
||||||
|
<CaptionSettingsView id={id} />
|
||||||
|
</Context.Card>
|
||||||
|
</OverlayPage>
|
||||||
<OverlayPage id={id} path="/source" width={343} height={431}>
|
<OverlayPage id={id} path="/source" width={343} height={431}>
|
||||||
<Context.Card>
|
<Context.Card>
|
||||||
<Context.BackLink onClick={() => router.navigate("/")}>
|
<Context.BackLink onClick={() => router.navigate("/")}>
|
||||||
|
@ -2,6 +2,7 @@ import { ReactNode, RefObject, useEffect, useRef } from "react";
|
|||||||
|
|
||||||
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
|
||||||
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
|
||||||
|
import { ProgressSaver } from "@/components/player/internals/ProgressSaver";
|
||||||
import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget";
|
import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget";
|
||||||
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
import { VideoContainer } from "@/components/player/internals/VideoContainer";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
@ -80,6 +81,7 @@ export function Container(props: PlayerProps) {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<BaseContainer>
|
<BaseContainer>
|
||||||
<VideoContainer />
|
<VideoContainer />
|
||||||
|
<ProgressSaver />
|
||||||
<div className="relative h-screen overflow-hidden">
|
<div className="relative h-screen overflow-hidden">
|
||||||
<VideoClickTarget />
|
<VideoClickTarget />
|
||||||
<HeadUpdater />
|
<HeadUpdater />
|
||||||
|
@ -24,6 +24,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
let isFullscreen = false;
|
let isFullscreen = false;
|
||||||
let isPausedBeforeSeeking = false;
|
let isPausedBeforeSeeking = false;
|
||||||
let isSeeking = false;
|
let isSeeking = false;
|
||||||
|
let startAt = 0;
|
||||||
|
|
||||||
function setupSource(vid: HTMLVideoElement, src: LoadableSource) {
|
function setupSource(vid: HTMLVideoElement, src: LoadableSource) {
|
||||||
if (src.type === "hls") {
|
if (src.type === "hls") {
|
||||||
@ -43,10 +44,12 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
|
|
||||||
hls.attachMedia(vid);
|
hls.attachMedia(vid);
|
||||||
hls.loadSource(src.url);
|
hls.loadSource(src.url);
|
||||||
|
vid.currentTime = startAt;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vid.src = src.url;
|
vid.src = src.url;
|
||||||
|
vid.currentTime = startAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSource() {
|
function setSource() {
|
||||||
@ -108,10 +111,11 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
destroyVideoElement();
|
destroyVideoElement();
|
||||||
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
|
fscreen.removeEventListener("fullscreenchange", fullscreenChange);
|
||||||
},
|
},
|
||||||
load(newSource) {
|
load(newSource, startAtInput) {
|
||||||
if (!newSource) unloadSource();
|
if (!newSource) unloadSource();
|
||||||
source = newSource;
|
source = newSource;
|
||||||
emit("loading", true);
|
emit("loading", true);
|
||||||
|
startAt = startAtInput;
|
||||||
setSource();
|
setSource();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export type DisplayInterfaceEvents = {
|
|||||||
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||||
play(): void;
|
play(): void;
|
||||||
pause(): void;
|
pause(): void;
|
||||||
load(source: LoadableSource | null): void;
|
load(source: LoadableSource | null, startAt: number): void;
|
||||||
processVideoElement(video: HTMLVideoElement): void;
|
processVideoElement(video: HTMLVideoElement): void;
|
||||||
processContainerElement(container: HTMLElement): void;
|
processContainerElement(container: HTMLElement): void;
|
||||||
toggleFullscreen(): void;
|
toggleFullscreen(): void;
|
||||||
|
@ -3,20 +3,38 @@ import { useInitializePlayer } from "@/components/player/hooks/useInitializePlay
|
|||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { SourceSliceSource } from "@/stores/player/utils/qualities";
|
import { SourceSliceSource } from "@/stores/player/utils/qualities";
|
||||||
|
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
|
||||||
|
|
||||||
export interface Source {
|
export interface Source {
|
||||||
url: string;
|
url: string;
|
||||||
type: MWStreamType;
|
type: MWStreamType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProgress(
|
||||||
|
items: Record<string, ProgressMediaItem>,
|
||||||
|
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() {
|
export function usePlayer() {
|
||||||
const setStatus = usePlayerStore((s) => s.setStatus);
|
const setStatus = usePlayerStore((s) => s.setStatus);
|
||||||
const setMeta = usePlayerStore((s) => s.setMeta);
|
const setMeta = usePlayerStore((s) => s.setMeta);
|
||||||
const setSource = usePlayerStore((s) => s.setSource);
|
const setSource = usePlayerStore((s) => s.setSource);
|
||||||
const status = usePlayerStore((s) => s.status);
|
const status = usePlayerStore((s) => s.status);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
|
||||||
const reset = usePlayerStore((s) => s.reset);
|
const reset = usePlayerStore((s) => s.reset);
|
||||||
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const { init } = useInitializePlayer();
|
const { init } = useInitializePlayer();
|
||||||
|
const progressStore = useProgressStore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reset,
|
reset,
|
||||||
@ -25,7 +43,7 @@ export function usePlayer() {
|
|||||||
setMeta(m);
|
setMeta(m);
|
||||||
},
|
},
|
||||||
playMedia(source: SourceSliceSource) {
|
playMedia(source: SourceSliceSource) {
|
||||||
setSource(source);
|
setSource(source, getProgress(progressStore.items, meta));
|
||||||
setStatus(playerStatus.PLAYING);
|
setStatus(playerStatus.PLAYING);
|
||||||
init();
|
init();
|
||||||
},
|
},
|
||||||
|
@ -119,7 +119,7 @@ function LinkChevron(props: { children?: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<span className="text-white flex items-center font-medium">
|
<span className="text-white flex items-center font-medium">
|
||||||
{props.children}
|
{props.children}
|
||||||
<Icon className="text-xl ml-1" icon={Icons.CHEVRON_RIGHT} />
|
<Icon className="text-xl ml-1 -mr-1.5" icon={Icons.CHEVRON_RIGHT} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
39
src/components/player/internals/ProgressSaver.tsx
Normal file
39
src/components/player/internals/ProgressSaver.tsx
Normal file
@ -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;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { BrandPill } from "@/components/layout/BrandPill";
|
|||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
|
||||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export interface PlayerPartProps {
|
export interface PlayerPartProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@ -14,16 +15,19 @@ export interface PlayerPartProps {
|
|||||||
|
|
||||||
export function PlayerPart(props: PlayerPartProps) {
|
export function PlayerPart(props: PlayerPartProps) {
|
||||||
const { showTargets, showTouchTargets } = useShouldShowControls();
|
const { showTargets, showTouchTargets } = useShouldShowControls();
|
||||||
|
const status = usePlayerStore((s) => s.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container onLoad={props.onLoad}>
|
<Player.Container onLoad={props.onLoad}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<Player.BlackOverlay show={showTargets} />
|
<Player.BlackOverlay show={showTargets} />
|
||||||
|
|
||||||
|
{status === "playing" ? (
|
||||||
<Player.CenterControls>
|
<Player.CenterControls>
|
||||||
<Player.LoadingSpinner />
|
<Player.LoadingSpinner />
|
||||||
<Player.AutoPlayStart />
|
<Player.AutoPlayStart />
|
||||||
</Player.CenterControls>
|
</Player.CenterControls>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Player.CenterMobileControls
|
<Player.CenterMobileControls
|
||||||
className="text-white"
|
className="text-white"
|
||||||
|
@ -81,7 +81,7 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
get().display?.load(null);
|
get().display?.load(null, 0);
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.status = playerStatus.IDLE;
|
s.status = playerStatus.IDLE;
|
||||||
s.meta = null;
|
s.meta = null;
|
||||||
|
@ -41,7 +41,7 @@ export interface SourceSlice {
|
|||||||
currentQuality: SourceQuality | null;
|
currentQuality: SourceQuality | null;
|
||||||
meta: PlayerMeta | null;
|
meta: PlayerMeta | null;
|
||||||
setStatus(status: PlayerStatus): void;
|
setStatus(status: PlayerStatus): void;
|
||||||
setSource(stream: SourceSliceSource): void;
|
setSource(stream: SourceSliceSource, startAt: number): void;
|
||||||
switchQuality(quality: SourceQuality): void;
|
switchQuality(quality: SourceQuality): void;
|
||||||
setMeta(meta: PlayerMeta): void;
|
setMeta(meta: PlayerMeta): void;
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||||||
s.meta = meta;
|
s.meta = meta;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setSource(stream: SourceSliceSource) {
|
setSource(stream: SourceSliceSource, startAt: number) {
|
||||||
let qualities: string[] = [];
|
let qualities: string[] = [];
|
||||||
if (stream.type === "file") qualities = Object.keys(stream.qualities);
|
if (stream.type === "file") qualities = Object.keys(stream.qualities);
|
||||||
const store = get();
|
const store = get();
|
||||||
@ -97,7 +97,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||||||
s.currentQuality = loadableStream.quality;
|
s.currentQuality = loadableStream.quality;
|
||||||
});
|
});
|
||||||
|
|
||||||
store.display?.load(loadableStream.stream);
|
store.display?.load(loadableStream.stream, startAt);
|
||||||
},
|
},
|
||||||
switchQuality(quality) {
|
switchQuality(quality) {
|
||||||
const store = get();
|
const store = get();
|
||||||
@ -108,7 +108,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||||||
set((s) => {
|
set((s) => {
|
||||||
s.currentQuality = quality;
|
s.currentQuality = quality;
|
||||||
});
|
});
|
||||||
store.display?.load(selectedQuality);
|
store.display?.load(selectedQuality, store.progress.time);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
100
src/stores/progress/index.ts
Normal file
100
src/stores/progress/index.ts
Normal file
@ -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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
@ -66,6 +66,12 @@ module.exports = {
|
|||||||
light: "#2A2A71"
|
light: "#2A2A71"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
buttons: {
|
||||||
|
toggle: "#8D44D6",
|
||||||
|
toggleDisabled: "#202836"
|
||||||
|
},
|
||||||
|
|
||||||
// only used for body colors/textures
|
// only used for body colors/textures
|
||||||
background: {
|
background: {
|
||||||
main: "#0A0A10",
|
main: "#0A0A10",
|
||||||
|
Loading…
Reference in New Issue
Block a user