v3: added index default tab selection setting, fixed notification pages not loading, reduced invite token length, global player state survives page reloads, many visual fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
This commit is contained in:
@@ -11,6 +11,12 @@ export interface PlayerContextValue {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
|
||||
// Initial seek offset for the active item (non-zero only for an item restored
|
||||
// from a previous session) and whether the active MediaPlayer should autoplay.
|
||||
// A restored item appears paused; a fresh play() autoplays on the user gesture.
|
||||
startTime: number;
|
||||
autoplay: boolean;
|
||||
|
||||
// Control — callable by any consumer
|
||||
play(item: PlayerItem): void;
|
||||
stop(): void;
|
||||
@@ -32,6 +38,8 @@ export const PlayerContext = createContext<PlayerContextValue>({
|
||||
playing: false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
startTime: 0,
|
||||
autoplay: false,
|
||||
play: () => {},
|
||||
stop: () => {},
|
||||
seekTo: () => {},
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PlayerContext, type PlayerItem } from "./PlayerContext.ts";
|
||||
|
||||
const STORAGE_KEY = "player";
|
||||
|
||||
// Snapshot persisted across reloads: which item was playing and how far in.
|
||||
interface StoredSession {
|
||||
item: PlayerItem;
|
||||
time: number;
|
||||
}
|
||||
|
||||
function readSession(): StoredSession | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<StoredSession>;
|
||||
if (!parsed.item || (parsed.item.kind !== "embed" && parsed.item.kind !== "file")) {
|
||||
return null;
|
||||
}
|
||||
return { item: parsed.item as PlayerItem, time: Number(parsed.time) || 0 };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [current, setCurrent] = useState<PlayerItem | null>(null);
|
||||
const restored = useRef(readSession()).current;
|
||||
|
||||
const [current, setCurrent] = useState<PlayerItem | null>(restored?.item ?? null);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(restored?.time ?? 0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
// Resume offset for the active item — only the restored item carries one.
|
||||
const [startTime, setStartTime] = useState(restored?.time ?? 0);
|
||||
// Restored items start paused (no user gesture); fresh play() autoplays.
|
||||
const [autoplay, setAutoplay] = useState(false);
|
||||
|
||||
// GlobalPlayer registers the active MediaPlayer's imperative handles here
|
||||
const seekRef = useRef<((t: number) => void) | null>(null);
|
||||
@@ -20,6 +48,8 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
setCurrent(item);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setStartTime(0);
|
||||
setAutoplay(true);
|
||||
setPlaying(false);
|
||||
}, []);
|
||||
|
||||
@@ -28,6 +58,7 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
setPlaying(false);
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
setStartTime(0);
|
||||
}, []);
|
||||
|
||||
const seekTo = useCallback((t: number) => {
|
||||
@@ -50,11 +81,45 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
setDuration(d);
|
||||
}, []);
|
||||
|
||||
// ── Persistence ────────────────────────────────────────────────────────────────
|
||||
// Write the item to storage as soon as it changes (survives crashes), then refresh
|
||||
// the playback position on pagehide — which also fires on a normal reload — so the
|
||||
// resume offset is accurate without thrashing localStorage on every timeupdate.
|
||||
useEffect(() => {
|
||||
if (current) {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ item: current, time: currentTime } satisfies StoredSession),
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
// currentTime intentionally omitted from deps — see pagehide writer below.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [current]);
|
||||
|
||||
const sessionRef = useRef<StoredSession | null>(null);
|
||||
sessionRef.current = current ? { item: current, time: currentTime } : null;
|
||||
|
||||
useEffect(() => {
|
||||
const persist = () => {
|
||||
if (sessionRef.current) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sessionRef.current));
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
globalThis.addEventListener("pagehide", persist);
|
||||
return () => globalThis.removeEventListener("pagehide", persist);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
current,
|
||||
playing,
|
||||
currentTime,
|
||||
duration,
|
||||
startTime,
|
||||
autoplay,
|
||||
play,
|
||||
stop,
|
||||
seekTo,
|
||||
@@ -68,6 +133,8 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
playing,
|
||||
currentTime,
|
||||
duration,
|
||||
startTime,
|
||||
autoplay,
|
||||
play,
|
||||
stop,
|
||||
seekTo,
|
||||
|
||||
Reference in New Issue
Block a user