mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(orbit,playback): move feature-owned stores (helpModal→orbit; libraryPlaybackHint+playerBarLayout→playback)
This commit is contained in:
@@ -9,7 +9,7 @@ import { resolveQueueTrack } from '@/features/playback/store/queueTrackView';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { setDeferHotCachePrefetch } from '@/utils/cache/hotCacheGate';
|
||||
import { notifyLibraryPlaybackHint } from '@/store/libraryPlaybackHint';
|
||||
import { notifyLibraryPlaybackHint } from '@/features/playback/store/libraryPlaybackHint';
|
||||
import {
|
||||
playListenSessionFinalize,
|
||||
playListenSessionOnProgress,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { librarySetPlaybackHint, type PlaybackHint } from '@/lib/api/library';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||
|
||||
/**
|
||||
* Bridge from the audio lifecycle to the Rust library scheduler's
|
||||
* bandwidth lane (spec §6.2.4, PR-5 kickoff Q3 — JS pushes the hint).
|
||||
* Only fires when the local index is enabled for the active server,
|
||||
* and dedupes repeated identical hints so we don't spam the IPC
|
||||
* boundary on every progress tick.
|
||||
*/
|
||||
let lastHint: PlaybackHint | null = null;
|
||||
|
||||
export function notifyLibraryPlaybackHint(hint: PlaybackHint): void {
|
||||
const activeId = useAuthStore.getState().activeServerId;
|
||||
if (!useLibraryIndexStore.getState().isIndexEnabled(activeId)) {
|
||||
lastHint = null;
|
||||
return;
|
||||
}
|
||||
if (lastHint === hint) return;
|
||||
lastHint = hint;
|
||||
void librarySetPlaybackHint(hint).catch(() => {
|
||||
/* best-effort — scheduler falls back to Idle parallelism */
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import {
|
||||
DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
|
||||
usePlayerBarLayoutStore,
|
||||
} from '@/features/playback/store/playerBarLayoutStore';
|
||||
|
||||
describe('playerBarLayoutStore', () => {
|
||||
beforeEach(() => {
|
||||
usePlayerBarLayoutStore.getState().reset();
|
||||
});
|
||||
|
||||
it('starts with all six items visible in declared order', () => {
|
||||
const items = usePlayerBarLayoutStore.getState().items;
|
||||
expect(items.map(i => i.id)).toEqual([
|
||||
'starRating', 'favorite', 'lastfmLove', 'playbackRate', 'equalizer', 'miniPlayer',
|
||||
]);
|
||||
expect(items.every(i => i.visible)).toBe(true);
|
||||
});
|
||||
|
||||
it('toggleItem flips the matching id without disturbing the others', () => {
|
||||
usePlayerBarLayoutStore.getState().toggleItem('equalizer');
|
||||
const items = usePlayerBarLayoutStore.getState().items;
|
||||
expect(items.find(i => i.id === 'equalizer')?.visible).toBe(false);
|
||||
expect(items.find(i => i.id === 'starRating')?.visible).toBe(true);
|
||||
expect(items.find(i => i.id === 'miniPlayer')?.visible).toBe(true);
|
||||
});
|
||||
|
||||
it('reset restores defaults after toggles', () => {
|
||||
const { toggleItem, reset } = usePlayerBarLayoutStore.getState();
|
||||
toggleItem('favorite');
|
||||
toggleItem('lastfmLove');
|
||||
reset();
|
||||
expect(usePlayerBarLayoutStore.getState().items).toEqual(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export type PlayerBarLayoutItemId =
|
||||
| 'starRating'
|
||||
| 'favorite'
|
||||
// 'lastfmLove' is the enrichment-primary love button. The id is kept (not
|
||||
// renamed to 'networkLove') because it is persisted in user layouts — renaming
|
||||
// would silently drop the button from existing configs. Label is provider-neutral.
|
||||
| 'lastfmLove'
|
||||
| 'playbackRate'
|
||||
| 'equalizer'
|
||||
| 'miniPlayer';
|
||||
|
||||
export interface PlayerBarLayoutItemConfig {
|
||||
id: PlayerBarLayoutItemId;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PLAYER_BAR_LAYOUT_ITEMS: PlayerBarLayoutItemConfig[] = [
|
||||
{ id: 'starRating', visible: true },
|
||||
{ id: 'favorite', visible: true },
|
||||
{ id: 'lastfmLove', visible: true },
|
||||
{ id: 'playbackRate', visible: true },
|
||||
{ id: 'equalizer', visible: true },
|
||||
{ id: 'miniPlayer', visible: true },
|
||||
];
|
||||
|
||||
interface PlayerBarLayoutStore {
|
||||
items: PlayerBarLayoutItemConfig[];
|
||||
setItems: (items: PlayerBarLayoutItemConfig[]) => void;
|
||||
toggleItem: (id: PlayerBarLayoutItemId) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const usePlayerBarLayoutStore = create<PlayerBarLayoutStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
|
||||
|
||||
setItems: (items) => set({ items }),
|
||||
|
||||
toggleItem: (id) => set((s) => ({
|
||||
items: s.items.map(it => it.id === id ? { ...it, visible: !it.visible } : it),
|
||||
})),
|
||||
|
||||
reset: () => set({ items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS }),
|
||||
}),
|
||||
{
|
||||
name: 'psysonic_player_bar_layout',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return;
|
||||
const knownIds = new Set(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS.map(i => i.id));
|
||||
const safe = (state.items ?? [])
|
||||
.filter((i): i is PlayerBarLayoutItemConfig =>
|
||||
i != null && typeof i.id === 'string' && knownIds.has(i.id as PlayerBarLayoutItemId));
|
||||
const seen = new Set(safe.map(i => i.id));
|
||||
const missing = DEFAULT_PLAYER_BAR_LAYOUT_ITEMS.filter(i => !seen.has(i.id));
|
||||
state.items = missing.length > 0 ? [...safe, ...missing] : safe;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user