Skip to content

Commit 2db252b

Browse files
committed
fix: make clear recent history actually persist
Prevent the player and session restore from re-writing history after a clear by wiping last session and cached audio, and suppressing history writes until the user plays something new.
1 parent b726dfd commit 2db252b

3 files changed

Lines changed: 39 additions & 11 deletions

File tree

src/components/HistoryList.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { useAppContext } from "@/context/AppContext";
1818
import type { HistoryItem } from "@/interfaces";
1919
import { historyItemSourceUrl, resolveCachedMedia } from "@/lib/playFromCache";
2020
import { timeAgo } from "@/utils";
21-
import { clearMediaCache } from "@/utils/cache";
2221

2322
interface HistoryListProps {
2423
onPlay?: () => void;
@@ -31,7 +30,7 @@ export default function HistoryList({
3130
maxHeight,
3231
showClear = true,
3332
}: HistoryListProps) {
34-
const { history, setHistory, openPlayer } = useAppContext();
33+
const { history, clearHistory, openPlayer } = useAppContext();
3534
const [confirmOpen, setConfirmOpen] = useState(false);
3635
const theme = useMantineTheme();
3736

@@ -97,9 +96,7 @@ export default function HistoryList({
9796
<Button
9897
color="red"
9998
onClick={() => {
100-
void clearMediaCache();
101-
setHistory([]);
102-
setConfirmOpen(false);
99+
void clearHistory().finally(() => setConfirmOpen(false));
103100
}}
104101
>
105102
Clear all

src/components/Player.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,13 @@ export function Player({
149149
}) {
150150
const theme = useMantineTheme();
151151
const isMobile = useMediaQuery("(max-width: 1024px)");
152-
const { media: contextMedia, setMedia, setHistory, setTheme } = useAppContext();
152+
const {
153+
media: contextMedia,
154+
setMedia,
155+
setHistory,
156+
setTheme,
157+
isHistoryWriteAllowed,
158+
} = useAppContext();
153159
const isMini = mode === "mini";
154160

155161
// Phase management: extracting while URL is being resolved, then playing
@@ -350,6 +356,7 @@ export function Player({
350356

351357
// Persist history once media is playable; refresh cover/title when extraction completes
352358
useEffect(() => {
359+
if (!isHistoryWriteAllowed()) return;
353360
if (!media?.sourceUrl || !media.fileUrl) return;
354361
// Local uploads are session-only — never write them to history
355362
if (media.sourceUrl.startsWith("local:")) return;
@@ -390,7 +397,7 @@ export function Player({
390397
next[existingIdx] = nextItem;
391398
return next;
392399
});
393-
}, [media, setHistory]);
400+
}, [media, setHistory, isHistoryWriteAllowed]);
394401

395402
// Use sourceUrl from media state
396403
const sourceUrl = media?.sourceUrl ?? url ?? "";
@@ -880,7 +887,7 @@ export function Player({
880887
advancedStretch,
881888
lyrics: lyricsSettings,
882889
});
883-
if (!sourceUrl.startsWith("local:")) {
890+
if (!sourceUrl.startsWith("local:") && isHistoryWriteAllowed()) {
884891
patchLastSession(sourceUrl, { positionSeconds: currentTime });
885892
}
886893
}
@@ -913,17 +920,19 @@ export function Player({
913920
customSemitones,
914921
lyricsSettings,
915922
currentTime,
923+
isHistoryWriteAllowed,
916924
]);
917925

918926
// Throttled progress persistence while the track is active
919927
useEffect(() => {
928+
if (!isHistoryWriteAllowed()) return;
920929
if (!sourceUrl || sourceUrl.startsWith("local:") || !stateLoaded) return;
921930
if (!Number.isFinite(currentTime) || currentTime < 0) return;
922931
const id = window.setTimeout(() => {
923932
patchLastSession(sourceUrl, { positionSeconds: currentTime });
924933
}, 1500);
925934
return () => window.clearTimeout(id);
926-
}, [sourceUrl, currentTime, stateLoaded]);
935+
}, [sourceUrl, currentTime, stateLoaded, isHistoryWriteAllowed]);
927936

928937
// Modal controls
929938
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);

src/context/AppContext.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { clearLastSession, loadLastSession, saveLastSession } from "@/lib/lastSe
1515
import { mediaFromLocalCache } from "@/lib/playFromCache";
1616
import { playerPathForMedia, softReplaceUrl } from "@/lib/playerNavigation";
1717
import { appTheme } from "@/lib/theme";
18+
import { clearMediaCache } from "@/utils/cache";
1819

1920
const HISTORY_STORAGE_KEY = "moonlit-history";
2021

@@ -42,6 +43,8 @@ export interface OpenPlayerOptions {
4243
autoPlay?: boolean;
4344
/** Resume playback head (seconds). Used by session restore. */
4445
resumePosition?: number;
46+
/** When false, do not resume history writes (session restore). Default true. */
47+
recordHistory?: boolean;
4548
}
4649

4750
interface AppContextValue {
@@ -64,7 +67,8 @@ interface AppContextValue {
6467
// History state (persisted to localStorage)
6568
history: HistoryItem[];
6669
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>;
67-
clearHistory: () => void;
70+
clearHistory: () => Promise<void>;
71+
isHistoryWriteAllowed: () => boolean;
6872

6973
// Theme state
7074
theme: MantineThemeOverride;
@@ -88,6 +92,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
8892
const mediaRef = useRef(media);
8993
const playerUrlRef = useRef(playerUrl);
9094
const restoredRef = useRef(false);
95+
const skipHistoryWritesRef = useRef(false);
9196

9297
useEffect(() => {
9398
mediaRef.current = media;
@@ -136,15 +141,28 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
136141
[],
137142
);
138143

139-
const clearHistory = useCallback(() => {
144+
const clearHistory = useCallback(async () => {
145+
skipHistoryWritesRef.current = true;
140146
setHistoryState([]);
147+
try {
148+
localStorage.removeItem(HISTORY_STORAGE_KEY);
149+
} catch (e) {
150+
console.error("Failed to clear history:", e);
151+
}
152+
clearLastSession();
153+
await clearMediaCache();
141154
}, []);
142155

156+
const isHistoryWriteAllowed = useCallback(() => !skipHistoryWritesRef.current, []);
157+
143158
const setMedia = useCallback((next: Media | null) => {
144159
setMediaState(next);
145160
}, []);
146161

147162
const openPlayer = useCallback((options: OpenPlayerOptions = {}) => {
163+
if (options.recordHistory !== false) {
164+
skipHistoryWritesRef.current = false;
165+
}
148166
const expand = options.expand !== false;
149167
setPlayerAutoPlay(options.autoPlay !== false);
150168
setPlayerResumeAt(
@@ -244,6 +262,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
244262
autoPlay: false,
245263
syncUrl: true,
246264
resumePosition: session.positionSeconds ?? 0,
265+
recordHistory: false,
247266
});
248267
return;
249268
}
@@ -254,6 +273,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
254273
autoPlay: false,
255274
syncUrl: true,
256275
resumePosition: session.positionSeconds ?? 0,
276+
recordHistory: false,
257277
});
258278
})();
259279
}, 0);
@@ -263,6 +283,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
263283
// Persist last session while a remote track is active
264284
useEffect(() => {
265285
if (!isHydrated) return;
286+
if (skipHistoryWritesRef.current) return;
266287
if (playerMode === "hidden") return;
267288
const sourceUrl = playerUrl || media?.sourceUrl;
268289
if (!sourceUrl) return;
@@ -296,6 +317,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
296317
history,
297318
setHistory,
298319
clearHistory,
320+
isHistoryWriteAllowed,
299321
theme,
300322
setTheme,
301323
}}

0 commit comments

Comments
 (0)