diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 34937f60e..b47ad54c5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -721,6 +721,10 @@ "autoDJ_itemCount_description": "the number of items attempted to be added to the queue when auto DJ is enabled", "autoDJ_timing": "timing", "autoDJ_timing_description": "the number of songs remaining in the queue before auto DJ is triggered", + "autosave": "automatically save play queue", + "autosave_description": "enable automatically saving the play queue to your server. this is only possible when using Navidrome/Subsonic, and you cannot have a mixed play queue.", + "autosaveCount": "automatic play queue save frequency", + "autosaveCount_description": "how many track changes before the queue is saved. 1 (minimum) means every song change", "accentColor_description": "sets the accent color for the application", "accentColor": "accent color", "useThemeAccentColor": "use theme accent color", diff --git a/src/renderer/features/player/components/audio-players.tsx b/src/renderer/features/player/components/audio-players.tsx index 9a3e039c9..d4d667a50 100644 --- a/src/renderer/features/player/components/audio-players.tsx +++ b/src/renderer/features/player/components/audio-players.tsx @@ -9,6 +9,7 @@ import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player'; import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player'; import { SleepTimerHook } from '/@/renderer/features/player/components/sleep-timer-button'; import { AutoDJHook } from '/@/renderer/features/player/hooks/use-auto-dj'; +import { AutosaveHook } from '/@/renderer/features/player/hooks/use-autosave'; import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-session'; import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris'; import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys'; @@ -64,6 +65,7 @@ export const AudioPlayers = () => { + { + const server = useCurrentServer(); + const currentSong = usePlayerSong(); + const priorSongId = useRef(undefined); + const songCount = useRef(0); + const { count, enabled } = useSettingsStore((state) => state.general.autoSave); + const { mutate: savePlayQueue } = useSaveQueue(); + + useEffect(() => { + if (enabled && server.type !== ServerType.JELLYFIN) { + if (currentSong?._uniqueId !== priorSongId.current) { + if (songCount.current === count) { + savePlayQueue(); + songCount.current = 1; + } else { + songCount.current += 1; + } + + priorSongId.current = currentSong?._uniqueId; + } + } + }, [enabled, count, currentSong?._uniqueId, savePlayQueue, server.type]); +}; + +export const AutosaveHook = () => { + useAutosave(); + return null; +}; diff --git a/src/renderer/features/player/hooks/use-queue-restore.ts b/src/renderer/features/player/hooks/use-queue-restore.ts index 146018903..bd3b38ab3 100644 --- a/src/renderer/features/player/hooks/use-queue-restore.ts +++ b/src/renderer/features/player/hooks/use-queue-restore.ts @@ -89,8 +89,7 @@ export const useSaveQueue = () => { }); toast.success({ - message: '', - title: t('form.saveQueue.success', { postProcess: 'sentenceCase' }), + message: t('form.saveQueue.success', { postProcess: 'sentenceCase' }), }); } catch (error) { toast.error({ diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 5466548e7..5c4e9692c 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -688,6 +688,59 @@ export const ApplicationSettings = memo(() => { isHidden: false, title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + autoSave: { + ...settings.autoSave, + enabled: e.currentTarget.checked, + }, + }, + }); + }} + /> + ), + description: t('setting.autosave', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.autosave', { postProcess: 'sentenceCase' }), + }, + { + control: ( + { + if (!e) return; + const newVal = e.currentTarget.value + ? Math.max(Number(e.currentTarget.value), 1) + : settings.autoSave.count; + setSettings({ + general: { + ...settings, + autoSave: { + ...settings.autoSave, + count: newVal, + }, + }, + }); + }} + value={settings.autoSave.count} + /> + ), + description: t('setting.autosaveCount', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !settings.autoSave.enabled, + title: t('setting.autosaveCount', { postProcess: 'sentenceCase' }), + }, ]; return ( diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index ba3246af7..e49342834 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -430,6 +430,11 @@ export enum HomeFeatureStyle { SINGLE = 'single', } +const AutoSaveSchema = z.object({ + count: z.number().min(0), + enabled: z.boolean(), +}); + export const GeneralSettingsSchema = z.object({ accent: z .string() @@ -446,6 +451,7 @@ export const GeneralSettingsSchema = z.object({ artistItems: z.array(SortableItemSchema(ArtistItemSchema)), artistRadioCount: z.number(), artistReleaseTypeItems: z.array(SortableItemSchema(ArtistReleaseTypeItemSchema)), + autoSave: AutoSaveSchema, blurExplicitImages: z.boolean(), buttonSize: z.number(), collections: z.array(CollectionSchema), @@ -1094,6 +1100,10 @@ const initialState: SettingsState = { artistItems, artistRadioCount: 20, artistReleaseTypeItems, + autoSave: { + count: 10, + enabled: false, + }, blurExplicitImages: false, buttonSize: 15, collections: [],