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

This commit is contained in:
khannurien
2026-06-27 19:44:35 +00:00
parent 88f1eb3d03
commit c29f45fbc8
25 changed files with 733 additions and 278 deletions

View File

@@ -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: () => {},

View File

@@ -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,