diff --git a/src/renderer/features/player/components/audio-players.tsx b/src/renderer/features/player/components/audio-players.tsx index cb5485e83..1ada81279 100644 --- a/src/renderer/features/player/components/audio-players.tsx +++ b/src/renderer/features/player/components/audio-players.tsx @@ -14,7 +14,10 @@ import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-se import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris'; import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys'; import { PowerSaveBlockerHook } from '/@/renderer/features/player/hooks/use-power-save-blocker'; -import { QueueRestoreTimestampHook } from '/@/renderer/features/player/hooks/use-queue-restore'; +import { + InitialTimestampRestoreHook, + QueueRestoreTimestampHook, +} from '/@/renderer/features/player/hooks/use-queue-restore'; import { ScrobbleHook } from '/@/renderer/features/player/hooks/use-scrobble'; import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song'; import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio'; @@ -134,6 +137,7 @@ export const AudioPlayers = () => { + diff --git a/src/renderer/features/player/hooks/use-queue-restore.ts b/src/renderer/features/player/hooks/use-queue-restore.ts index 74c280e7a..9b06feebf 100644 --- a/src/renderer/features/player/hooks/use-queue-restore.ts +++ b/src/renderer/features/player/hooks/use-queue-restore.ts @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { t } from 'i18next'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { api } from '/@/renderer/api'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; @@ -9,13 +9,18 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; import { setTimestamp, useCurrentServerId, + usePlayerActions, + usePlayerHydrated, + usePlayerSong, + usePlayerStatus, usePlayerStore, useTimestampStoreBase, } from '/@/renderer/store'; import { toast } from '/@/shared/components/toast/toast'; +import { PlayerStatus } from '/@/shared/types/types'; export const useQueueRestoreTimestamp = () => { - const player = usePlayerStore(); + const { mediaSeekToTimestamp } = usePlayerActions(); usePlayerEvents( { @@ -24,7 +29,7 @@ export const useQueueRestoreTimestamp = () => { setTimeout(() => { setTimestamp(position); - player.mediaSeekToTimestamp(position); + mediaSeekToTimestamp(position); }, 100); }, }, @@ -37,6 +42,72 @@ export const QueueRestoreTimestampHook = () => { return null; }; +export const useInitialTimestampRestore = () => { + const { mediaSeekToTimestamp } = usePlayerActions(); + const playerHydrated = usePlayerHydrated(); + const currentSong = usePlayerSong(); + const playerStatus = usePlayerStatus(); + const timestamp = useTimestampStoreBase((state) => state.timestamp); + + const startupRestoreInitializedRef = useRef(false); + const startupSeekArmedRef = useRef(null); + const startupSeekAppliedRef = useRef(false); + + const applyStartupSeek = useCallback(() => { + if (startupSeekAppliedRef.current) { + return; + } + + const seekTimestamp = startupSeekArmedRef.current; + if (!seekTimestamp || seekTimestamp <= 0) { + return; + } + + startupSeekAppliedRef.current = true; + startupSeekArmedRef.current = null; + + setTimeout(() => { + mediaSeekToTimestamp(seekTimestamp); + }, 100); + }, [mediaSeekToTimestamp]); + + useEffect(() => { + if (startupRestoreInitializedRef.current) { + return; + } + + if (!playerHydrated || !currentSong) { + return; + } + + startupRestoreInitializedRef.current = true; + + if (timestamp > 0) { + startupSeekArmedRef.current = timestamp; + } + + if (playerStatus === PlayerStatus.PLAYING) { + applyStartupSeek(); + } + }, [applyStartupSeek, currentSong, playerHydrated, playerStatus, timestamp]); + + usePlayerEvents( + { + onPlayerStatus: (properties) => { + if (properties.status === PlayerStatus.PLAYING) { + applyStartupSeek(); + } + }, + }, + [applyStartupSeek], + ); +}; + +export const InitialTimestampRestoreHook = () => { + useInitialTimestampRestore(); + return null; +}; + export const useSaveQueue = () => { const serverId = useCurrentServerId(); diff --git a/src/renderer/store/timestamp.store.ts b/src/renderer/store/timestamp.store.ts index 9e6581158..3b729eff0 100644 --- a/src/renderer/store/timestamp.store.ts +++ b/src/renderer/store/timestamp.store.ts @@ -1,4 +1,5 @@ -import { subscribeWithSelector } from 'zustand/middleware'; +import { del, get, set } from 'idb-keyval'; +import { persist, subscribeWithSelector } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; interface TimestampState { @@ -6,13 +7,36 @@ interface TimestampState { timestamp: number; } +const timestampStorage = { + getItem: async (name: string) => { + const value = await get(name); + if (value === undefined) { + return null; + } + return { state: { timestamp: value }, version: 1 } as const; + }, + removeItem: async (name: string) => { + await del(name); + }, + setItem: async (name: string, value: { state: { timestamp: number }; version?: number }) => { + await set(name, value.state.timestamp); + }, +}; + export const useTimestampStoreBase = createWithEqualityFn()( - subscribeWithSelector((set) => ({ - setTimestamp: (timestamp: number) => { - set({ timestamp }); + persist( + subscribeWithSelector((set) => ({ + setTimestamp: (timestamp: number) => { + set({ timestamp }); + }, + timestamp: 0, + })), + { + name: 'player-timestamp', + storage: timestampStorage, + version: 1, }, - timestamp: 0, - })), + ), ); export const subscribePlayerProgress = (