mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 20:49:47 +02:00
remove old queue
This commit is contained in:
+1
-1
@@ -64,7 +64,7 @@
|
|||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"test:queue-actions": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/virtualPlaybackWindow.test.mts src/components/queue/queueActions.test.mts src/components/queue/queuePerformance.test.mts src/components/queue/virtualQueuePaging.test.mts src/components/swipeableRowState.test.mts",
|
"test:queue-actions": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/virtualPlaybackWindow.test.mts src/components/swipeableRowState.test.mts",
|
||||||
"test:desktop-remote": "node --experimental-strip-types --test src/services/desktopRemotePairing.test.mts src/services/desktopRemoteTransport.test.mts",
|
"test:desktop-remote": "node --experimental-strip-types --test src/services/desktopRemotePairing.test.mts src/services/desktopRemoteTransport.test.mts",
|
||||||
"test:dynamic-playlists": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/playlists/dynamicPlaylist.test.mts",
|
"test:dynamic-playlists": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/playlists/dynamicPlaylist.test.mts",
|
||||||
"test:album-grouping": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/library/albumGrouping.test.mts src/shared/library/albumEligibility.test.mts src/library/albumIdentity.test.mts src/library/albumSummary.test.mts",
|
"test:album-grouping": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/shared/library/albumGrouping.test.mts src/shared/library/albumEligibility.test.mts src/library/albumIdentity.test.mts src/library/albumSummary.test.mts",
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ import { useEffect } from 'react';
|
|||||||
import { InteractionManager } from 'react-native';
|
import { InteractionManager } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import {
|
import {
|
||||||
SettingsCard,
|
|
||||||
SettingsNavRow,
|
SettingsNavRow,
|
||||||
SettingsSectionLabel,
|
SettingsSectionLabel,
|
||||||
SettingsSectionScreen,
|
SettingsSectionScreen,
|
||||||
SettingsToggleRow,
|
|
||||||
} from '@/components/settings/SettingsSectionScaffold';
|
} from '@/components/settings/SettingsSectionScaffold';
|
||||||
import { formatRelativeTime } from '@/lib/format';
|
import { formatRelativeTime } from '@/lib/format';
|
||||||
import { useColors } from '@/theme/themed';
|
import { useColors } from '@/theme/themed';
|
||||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||||
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
||||||
import { useSettingsStore } from '@/stores/settingsStore';
|
|
||||||
|
|
||||||
export default function ExperimentalSettingsScreen() {
|
export default function ExperimentalSettingsScreen() {
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
@@ -23,8 +20,6 @@ export default function ExperimentalSettingsScreen() {
|
|||||||
const desktopSyncStatus = useDesktopSyncStore((s) => s.status);
|
const desktopSyncStatus = useDesktopSyncStore((s) => s.status);
|
||||||
const desktopLastSyncAt = useDesktopSyncStore((s) => s.lastSyncAt);
|
const desktopLastSyncAt = useDesktopSyncStore((s) => s.lastSyncAt);
|
||||||
const desktopSyncConflictCount = useDesktopSyncStore((s) => s.conflicts.length);
|
const desktopSyncConflictCount = useDesktopSyncStore((s) => s.conflicts.length);
|
||||||
const nativeQueueEnabled = useSettingsStore((s) => s.nativeQueueEnabled);
|
|
||||||
const setNativeQueueEnabled = useSettingsStore((s) => s.setNativeQueueEnabled);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const task = InteractionManager.runAfterInteractions(() => {
|
const task = InteractionManager.runAfterInteractions(() => {
|
||||||
@@ -49,17 +44,7 @@ export default function ExperimentalSettingsScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsSectionScreen title="Experimental">
|
<SettingsSectionScreen title="Experimental">
|
||||||
<SettingsSectionLabel>PLAYBACK</SettingsSectionLabel>
|
<SettingsSectionLabel>DESKTOP</SettingsSectionLabel>
|
||||||
<SettingsCard>
|
|
||||||
<SettingsToggleRow
|
|
||||||
title="Native queue"
|
|
||||||
description="Use the Kotlin queue sheet and RecyclerView. Disable to compare with the legacy React Native queue."
|
|
||||||
value={nativeQueueEnabled}
|
|
||||||
onValueChange={(enabled) => void setNativeQueueEnabled(enabled)}
|
|
||||||
/>
|
|
||||||
</SettingsCard>
|
|
||||||
|
|
||||||
<SettingsSectionLabel spaced>DESKTOP</SettingsSectionLabel>
|
|
||||||
<SettingsNavRow
|
<SettingsNavRow
|
||||||
icon="phone-portrait-outline"
|
icon="phone-portrait-outline"
|
||||||
title="Desktop Remote"
|
title="Desktop Remote"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface SelectionActionBarProps {
|
|||||||
onAddToPlaylist: () => void;
|
onAddToPlaylist: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bottom batch-action bar for library multi-select (QueueTray action-bar language). */
|
/** Bottom batch-action bar for library multi-select (native queue action-bar language). */
|
||||||
export function SelectionActionBar({
|
export function SelectionActionBar({
|
||||||
count,
|
count,
|
||||||
onPlayNext,
|
onPlayNext,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import { SegmentedControl } from '@/components/SegmentedControl';
|
import { SegmentedControl } from '@/components/SegmentedControl';
|
||||||
import { LyricsBand } from '@/components/lyrics/LyricsBand';
|
import { LyricsBand } from '@/components/lyrics/LyricsBand';
|
||||||
import { QueueTray } from '@/components/queue/QueueTray';
|
|
||||||
import { RemoteQueueSheet } from '@/components/queue/RemoteQueueSheet';
|
import { RemoteQueueSheet } from '@/components/queue/RemoteQueueSheet';
|
||||||
import { seekTo } from '@/audio/playbackController';
|
import { seekTo } from '@/audio/playbackController';
|
||||||
import { spacing } from '@/theme';
|
import { spacing } from '@/theme';
|
||||||
@@ -41,7 +40,6 @@ export function NowPlayingCompanionPane({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
const companion = useSettingsStore((s) => s.nowPlayingCompanion);
|
const companion = useSettingsStore((s) => s.nowPlayingCompanion);
|
||||||
const nativeQueueEnabled = useSettingsStore((s) => s.nativeQueueEnabled);
|
|
||||||
const setCompanion = useSettingsStore((s) => s.setNowPlayingCompanion);
|
const setCompanion = useSettingsStore((s) => s.setNowPlayingCompanion);
|
||||||
const currentTime = usePlayerStore((s) => (active && !desktopTarget ? s.currentTime : 0));
|
const currentTime = usePlayerStore((s) => (active && !desktopTarget ? s.currentTime : 0));
|
||||||
const duration = usePlayerStore((s) => (desktopTarget ? 0 : s.duration));
|
const duration = usePlayerStore((s) => (desktopTarget ? 0 : s.duration));
|
||||||
@@ -74,15 +72,11 @@ export function NowPlayingCompanionPane({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{companion === 'queue' ? (
|
{companion === 'queue' ? (
|
||||||
nativeQueueEnabled ? (
|
<AstraQueueView
|
||||||
<AstraQueueView
|
active={active}
|
||||||
active={active}
|
palette={toNativeQueuePalette(colors)}
|
||||||
palette={toNativeQueuePalette(colors)}
|
style={styles.nativeQueue}
|
||||||
style={styles.nativeQueue}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<QueueTray embedded onClose={noop} />
|
|
||||||
)
|
|
||||||
) : track ? (
|
) : track ? (
|
||||||
<NowPlayingTrackFadeThrough
|
<NowPlayingTrackFadeThrough
|
||||||
transitionKey={transitionTrackKey}
|
transitionKey={transitionTrackKey}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { Visualizer } from '@/components/Visualizer';
|
|||||||
import { LyricsView } from '@/components/lyrics/LyricsView';
|
import { LyricsView } from '@/components/lyrics/LyricsView';
|
||||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||||
import { PlaybackTargetPicker } from '@/components/PlaybackTargetPicker';
|
import { PlaybackTargetPicker } from '@/components/PlaybackTargetPicker';
|
||||||
import { QueueTray } from '@/components/queue/QueueTray';
|
|
||||||
import { RemoteQueueSheet } from '@/components/queue/RemoteQueueSheet';
|
import { RemoteQueueSheet } from '@/components/queue/RemoteQueueSheet';
|
||||||
import { TactilePressable } from '@/components/player/TactilePressable';
|
import { TactilePressable } from '@/components/player/TactilePressable';
|
||||||
import { ScopeRack } from '@/components/player/ScopeRack';
|
import { ScopeRack } from '@/components/player/ScopeRack';
|
||||||
@@ -180,7 +179,6 @@ export function NowPlayingOverlay() {
|
|||||||
const nowPlayingAccentSource = useThemeStore((s) => s.nowPlayingAccentSource);
|
const nowPlayingAccentSource = useThemeStore((s) => s.nowPlayingAccentSource);
|
||||||
const coverArtAccentMethod = useThemeStore((s) => s.coverArtAccentMethod);
|
const coverArtAccentMethod = useThemeStore((s) => s.coverArtAccentMethod);
|
||||||
const scopeMode = useSettingsStore((s) => s.scopeMode);
|
const scopeMode = useSettingsStore((s) => s.scopeMode);
|
||||||
const nativeQueueEnabled = useSettingsStore((s) => s.nativeQueueEnabled);
|
|
||||||
const scopeStageVisible = useSettingsStore((s) => s.scopeStageVisible);
|
const scopeStageVisible = useSettingsStore((s) => s.scopeStageVisible);
|
||||||
const setScopeStageVisible = useSettingsStore((s) => s.setScopeStageVisible);
|
const setScopeStageVisible = useSettingsStore((s) => s.setScopeStageVisible);
|
||||||
const scopeStyle = useSettingsStore((s) => s.nowPlayingScopeStyle);
|
const scopeStyle = useSettingsStore((s) => s.nowPlayingScopeStyle);
|
||||||
@@ -385,7 +383,7 @@ export function NowPlayingOverlay() {
|
|||||||
}
|
}
|
||||||
suspendPanForChildTransition();
|
suspendPanForChildTransition();
|
||||||
setQueueOpen(true);
|
setQueueOpen(true);
|
||||||
if (nativeQueueEnabled && !isDesktopTarget) {
|
if (!isDesktopTarget) {
|
||||||
void AstraQueue.present({ palette: toNativeQueuePalette(colors) }).catch((error) => {
|
void AstraQueue.present({ palette: toNativeQueuePalette(colors) }).catch((error) => {
|
||||||
console.warn('[queue] native presentation failed', error);
|
console.warn('[queue] native presentation failed', error);
|
||||||
setQueueOpen(false);
|
setQueueOpen(false);
|
||||||
@@ -752,17 +750,16 @@ export function NowPlayingOverlay() {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stable identity: QueueTray is memo'd, so a fresh arrow here would defeat it.
|
// Stable identity: RemoteQueueSheet is memo'd, so a fresh arrow would defeat it.
|
||||||
const closeQueue = useCallback(() => {
|
const closeQueue = useCallback(() => {
|
||||||
suspendPanForChildTransition();
|
suspendPanForChildTransition();
|
||||||
if (nativeQueueEnabled && !isDesktopTarget) AstraQueue.dismiss();
|
if (!isDesktopTarget) AstraQueue.dismiss();
|
||||||
setQueueOpen(false);
|
setQueueOpen(false);
|
||||||
// Shared values and the state setter remain stable for this overlay mount.
|
// Shared values and the state setter remain stable for this overlay mount.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isDesktopTarget, nativeQueueEnabled]);
|
}, [isDesktopTarget]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!nativeQueueEnabled) return undefined;
|
|
||||||
const dismissed = AstraQueue.addListener('onDismissed', () => {
|
const dismissed = AstraQueue.addListener('onDismissed', () => {
|
||||||
setQueueOpen(false);
|
setQueueOpen(false);
|
||||||
});
|
});
|
||||||
@@ -797,15 +794,15 @@ export function NowPlayingOverlay() {
|
|||||||
playbackRequest.remove();
|
playbackRequest.remove();
|
||||||
revision.remove();
|
revision.remove();
|
||||||
};
|
};
|
||||||
}, [nativeQueueEnabled]);
|
}, []);
|
||||||
|
|
||||||
// `colors` is accent-scoped to the cover art, so it changes on every track
|
// `colors` is accent-scoped to the cover art, so it changes on every track
|
||||||
// change. present() captured the palette once, which left an open sheet
|
// change. present() captured the palette once, which left an open sheet
|
||||||
// wearing the previous track's accent.
|
// wearing the previous track's accent.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!nativeQueueEnabled || isDesktopTarget || !queueOpen) return;
|
if (isDesktopTarget || !queueOpen) return;
|
||||||
AstraQueue.updatePalette(toNativeQueuePalette(colors));
|
AstraQueue.updatePalette(toNativeQueuePalette(colors));
|
||||||
}, [colors, isDesktopTarget, nativeQueueEnabled, queueOpen]);
|
}, [colors, isDesktopTarget, queueOpen]);
|
||||||
|
|
||||||
// Hardware back, innermost layer first: menu → queue tray → player. Registered
|
// Hardware back, innermost layer first: menu → queue tray → player. Registered
|
||||||
// only while open, so it sits above the focused screen's own handlers (LIFO)
|
// only while open, so it sits above the focused screen's own handlers (LIFO)
|
||||||
@@ -1749,13 +1746,10 @@ export function NowPlayingOverlay() {
|
|||||||
<SleepTimerControls />
|
<SleepTimerControls />
|
||||||
</AppSheet>
|
</AppSheet>
|
||||||
) : null}
|
) : null}
|
||||||
{queueOpen && !hasTabletCompanion && (
|
{/* The native queue presents its own dialog from AstraQueue.present(). */}
|
||||||
isDesktopTarget ? (
|
{queueOpen && !hasTabletCompanion && isDesktopTarget ? (
|
||||||
<RemoteQueueSheet onClose={closeQueue} />
|
<RemoteQueueSheet onClose={closeQueue} />
|
||||||
) : !nativeQueueEnabled ? (
|
) : null}
|
||||||
<QueueTray onClose={closeQueue} />
|
|
||||||
) : null
|
|
||||||
)}
|
|
||||||
<PlaybackTargetPicker
|
<PlaybackTargetPicker
|
||||||
visible={targetPickerOpen}
|
visible={targetPickerOpen}
|
||||||
onClose={() => setTargetPickerOpen(false)}
|
onClose={() => setTargetPickerOpen(false)}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
|||||||
// Read-only queue sheet for the Desktop Remote: the desktop's current +
|
// Read-only queue sheet for the Desktop Remote: the desktop's current +
|
||||||
// upcoming tracks, tap-to-play. Deliberately NOT QueueTray — that component is
|
// upcoming tracks, tap-to-play. Deliberately NOT the native queue — that reads
|
||||||
// welded to the local RNTP queue store (drag-reorder, swipe-remove,
|
// the local playback session straight out of Room (drag-reorder, swipe-remove,
|
||||||
// multi-select), none of which applies to a remote snapshot. Uses an INLINE
|
// multi-select), none of which applies to a remote snapshot. This is the only
|
||||||
// BottomSheet like QueueTray does — BottomSheetModal's portal does not work in
|
// remaining React Native queue surface. It uses an INLINE BottomSheet because
|
||||||
// this app's screen setups (see queue-tray-sheet gotcha).
|
// BottomSheetModal's portal does not work in this app's screen setups (see
|
||||||
|
// queue-tray-sheet gotcha).
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Pressable, View } from 'react-native';
|
import { Pressable, View } from 'react-native';
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import test from 'node:test';
|
|
||||||
import {
|
|
||||||
indexQueueEntriesByKey,
|
|
||||||
moveQueueEntry,
|
|
||||||
removeQueueEntryAt,
|
|
||||||
resolveSelectedQueueAction,
|
|
||||||
type KeyedQueueEntry,
|
|
||||||
} from './queueActions.ts';
|
|
||||||
|
|
||||||
function keyed(keys: string[]): KeyedQueueEntry[] {
|
|
||||||
return keys.map((key) => ({ key }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function entryKeys(entries: readonly KeyedQueueEntry[]): string[] {
|
|
||||||
return entries.map((entry) => entry.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PositionedQueueEntry extends KeyedQueueEntry {
|
|
||||||
absoluteIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function positioned(keys: string[], baseOffset: number): PositionedQueueEntry[] {
|
|
||||||
return keys.map((key, index) => ({ key, absoluteIndex: baseOffset + index }));
|
|
||||||
}
|
|
||||||
|
|
||||||
test('resolves each drag from the latest keyed queue order', () => {
|
|
||||||
let upcoming = keyed(['A', 'B', 'C', 'D']);
|
|
||||||
|
|
||||||
let currentIndices = indexQueueEntriesByKey(upcoming);
|
|
||||||
upcoming = moveQueueEntry(upcoming, currentIndices.B, 3);
|
|
||||||
assert.deepEqual(entryKeys(upcoming), ['A', 'C', 'D', 'B']);
|
|
||||||
|
|
||||||
currentIndices = indexQueueEntriesByKey(upcoming);
|
|
||||||
assert.equal(currentIndices.B, 3);
|
|
||||||
upcoming = moveQueueEntry(upcoming, currentIndices.B, 1);
|
|
||||||
assert.deepEqual(entryKeys(upcoming), ['A', 'B', 'C', 'D']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('resolves first, middle, and last swipe removals from the pre-mutation order', () => {
|
|
||||||
const scenarios = [
|
|
||||||
{ localIndex: 0, absoluteIndex: 7, remaining: ['B', 'C'] },
|
|
||||||
{ localIndex: 1, absoluteIndex: 8, remaining: ['A', 'C'] },
|
|
||||||
{ localIndex: 2, absoluteIndex: 9, remaining: ['A', 'B'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const scenario of scenarios) {
|
|
||||||
const action = removeQueueEntryAt(keyed(['A', 'B', 'C']), scenario.localIndex, 7);
|
|
||||||
if (!action) throw new Error('expected a remove action');
|
|
||||||
|
|
||||||
assert.equal(action.absoluteIndex, scenario.absoluteIndex);
|
|
||||||
assert.deepEqual(entryKeys(action.nextEntries), scenario.remaining);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('keeps the swiped remove index after the optimistic ref changes to the next order', () => {
|
|
||||||
let entriesRef = positioned(['A', 'B', 'C'], 1);
|
|
||||||
const localIndex = entriesRef.findIndex((entry) => entry.key === 'B');
|
|
||||||
const action = removeQueueEntryAt(entriesRef, localIndex, 1);
|
|
||||||
|
|
||||||
if (!action) throw new Error('expected a remove action');
|
|
||||||
entriesRef = action.nextEntries;
|
|
||||||
|
|
||||||
assert.equal(action.absoluteIndex, 2);
|
|
||||||
assert.deepEqual(entryKeys(entriesRef), ['A', 'C']);
|
|
||||||
// Looking up localIndex after the optimistic update would incorrectly select C.
|
|
||||||
assert.equal(entriesRef[localIndex].absoluteIndex, 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removes duplicate tracks by unique row key and preserves the other occurrence', () => {
|
|
||||||
const upcoming = keyed(['same-track:0', 'same-track:1', 'tail']);
|
|
||||||
const action = removeQueueEntryAt(upcoming, 1, 20);
|
|
||||||
|
|
||||||
if (!action) throw new Error('expected a remove action');
|
|
||||||
assert.equal(action.absoluteIndex, 21);
|
|
||||||
assert.deepEqual(entryKeys(action.nextEntries), ['same-track:0', 'tail']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses the loaded virtual-page offset for swipe removal', () => {
|
|
||||||
const action = removeQueueEntryAt(keyed(['V500', 'V501', 'V502']), 2, 500);
|
|
||||||
|
|
||||||
if (!action) throw new Error('expected a remove action');
|
|
||||||
assert.equal(action.absoluteIndex, 502);
|
|
||||||
assert.deepEqual(entryKeys(action.nextEntries), ['V500', 'V501']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('re-resolves consecutive removals against each optimistic order', () => {
|
|
||||||
let upcoming = keyed(['A', 'B', 'C', 'D']);
|
|
||||||
|
|
||||||
const first = removeQueueEntryAt(upcoming, 1, 10);
|
|
||||||
if (!first) throw new Error('expected the first remove action');
|
|
||||||
upcoming = first.nextEntries;
|
|
||||||
|
|
||||||
const second = removeQueueEntryAt(upcoming, 1, 10);
|
|
||||||
if (!second) throw new Error('expected the second remove action');
|
|
||||||
upcoming = second.nextEntries;
|
|
||||||
|
|
||||||
assert.equal(first.absoluteIndex, 11);
|
|
||||||
assert.equal(second.absoluteIndex, 11);
|
|
||||||
assert.deepEqual(entryKeys(upcoming), ['A', 'D']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('captures play-next absolute index before optimistic reordering', () => {
|
|
||||||
let entriesRef = positioned(['A', 'B', 'C'], 40);
|
|
||||||
const snapshot = entriesRef;
|
|
||||||
const localIndex = snapshot.findIndex((entry) => entry.key === 'C');
|
|
||||||
const absoluteIndex = snapshot[localIndex].absoluteIndex;
|
|
||||||
|
|
||||||
entriesRef = moveQueueEntry(snapshot, localIndex, 0);
|
|
||||||
|
|
||||||
assert.equal(absoluteIndex, 42);
|
|
||||||
assert.deepEqual(entryKeys(entriesRef), ['C', 'A', 'B']);
|
|
||||||
// Looking up localIndex after the optimistic update would incorrectly select B.
|
|
||||||
assert.equal(entriesRef[localIndex].absoluteIndex, 41);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('captures selected absolute indices before optimistic batch mutation', () => {
|
|
||||||
const upcoming = keyed(['A', 'B', 'C', 'D']);
|
|
||||||
const selectedKeys = new Set(['B', 'D']);
|
|
||||||
const action = resolveSelectedQueueAction(upcoming, selectedKeys, 1);
|
|
||||||
|
|
||||||
assert.deepEqual(action.absoluteIndices, [2, 4]);
|
|
||||||
assert.deepEqual(entryKeys(action.entriesWithoutSelected), ['A', 'C']);
|
|
||||||
assert.deepEqual(entryKeys(action.entriesWithSelectedFirst), ['B', 'D', 'A', 'C']);
|
|
||||||
|
|
||||||
const afterOptimisticRemoval = resolveSelectedQueueAction(
|
|
||||||
action.entriesWithoutSelected,
|
|
||||||
selectedKeys,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert.deepEqual(afterOptimisticRemoval.absoluteIndices, []);
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
export interface KeyedQueueEntry {
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QueueIndexByKey = Record<string, number>;
|
|
||||||
|
|
||||||
export interface QueueItemRemoveAction<T extends KeyedQueueEntry> {
|
|
||||||
absoluteIndex: number;
|
|
||||||
nextEntries: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectedQueueAction<T extends KeyedQueueEntry> {
|
|
||||||
absoluteIndices: number[];
|
|
||||||
entriesWithSelectedFirst: T[];
|
|
||||||
entriesWithoutSelected: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function indexQueueEntriesByKey<T extends KeyedQueueEntry>(
|
|
||||||
entries: readonly T[]
|
|
||||||
): QueueIndexByKey {
|
|
||||||
const out: QueueIndexByKey = {};
|
|
||||||
entries.forEach((entry, index) => {
|
|
||||||
out[entry.key] = index;
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moveQueueEntry<T>(entries: readonly T[], from: number, to: number): T[] {
|
|
||||||
const nextEntries = [...entries];
|
|
||||||
if (
|
|
||||||
from === to ||
|
|
||||||
from < 0 ||
|
|
||||||
to < 0 ||
|
|
||||||
from >= nextEntries.length ||
|
|
||||||
to >= nextEntries.length
|
|
||||||
) {
|
|
||||||
return nextEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [moved] = nextEntries.splice(from, 1);
|
|
||||||
nextEntries.splice(to, 0, moved);
|
|
||||||
return nextEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeQueueEntryAt<T extends KeyedQueueEntry>(
|
|
||||||
entries: readonly T[],
|
|
||||||
localIndex: number,
|
|
||||||
baseOffset: number
|
|
||||||
): QueueItemRemoveAction<T> | null {
|
|
||||||
if (localIndex < 0 || localIndex >= entries.length) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
absoluteIndex: baseOffset + localIndex,
|
|
||||||
nextEntries: entries.filter((_, index) => index !== localIndex),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSelectedQueueAction<T extends KeyedQueueEntry>(
|
|
||||||
entries: readonly T[],
|
|
||||||
selectedKeys: ReadonlySet<string>,
|
|
||||||
baseOffset: number
|
|
||||||
): SelectedQueueAction<T> {
|
|
||||||
const selectedEntries: T[] = [];
|
|
||||||
const remainingEntries: T[] = [];
|
|
||||||
const absoluteIndices: number[] = [];
|
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
|
||||||
if (selectedKeys.has(entry.key)) {
|
|
||||||
selectedEntries.push(entry);
|
|
||||||
absoluteIndices.push(baseOffset + index);
|
|
||||||
} else {
|
|
||||||
remainingEntries.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
absoluteIndices,
|
|
||||||
entriesWithSelectedFirst: [...selectedEntries, ...remainingEntries],
|
|
||||||
entriesWithoutSelected: remainingEntries,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import test from 'node:test';
|
|
||||||
import {
|
|
||||||
QUEUE_INITIAL_RENDER_AHEAD_ROWS,
|
|
||||||
QUEUE_INITIAL_RENDER_DISTANCE,
|
|
||||||
QUEUE_RENDER_AHEAD_MIN_ROWS,
|
|
||||||
QUEUE_RENDER_WINDOW_MULTIPLIER,
|
|
||||||
QUEUE_ROW_HEIGHT,
|
|
||||||
queuePreviewRowCount,
|
|
||||||
queueRenderDistance,
|
|
||||||
} from './queuePerformance.ts';
|
|
||||||
|
|
||||||
test('queue opens with the former four-row render-ahead distance', () => {
|
|
||||||
assert.equal(QUEUE_INITIAL_RENDER_AHEAD_ROWS, 4);
|
|
||||||
assert.equal(QUEUE_INITIAL_RENDER_DISTANCE, QUEUE_ROW_HEIGHT * 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('queue render-ahead covers two windows with a twelve-row minimum', () => {
|
|
||||||
assert.equal(QUEUE_RENDER_AHEAD_MIN_ROWS, 12);
|
|
||||||
assert.equal(QUEUE_RENDER_WINDOW_MULTIPLIER, 2);
|
|
||||||
assert.equal(queueRenderDistance(780), 1560);
|
|
||||||
assert.equal(queueRenderDistance(320), QUEUE_ROW_HEIGHT * 12);
|
|
||||||
assert.equal(queueRenderDistance(0), QUEUE_ROW_HEIGHT * 12);
|
|
||||||
assert.equal(queueRenderDistance(Number.NaN), QUEUE_ROW_HEIGHT * 12);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('initial queue preview covers the sheet viewport without duplicating a screen', () => {
|
|
||||||
assert.equal(queuePreviewRowCount(780), 4);
|
|
||||||
assert.equal(queuePreviewRowCount(900), 5);
|
|
||||||
assert.equal(queuePreviewRowCount(1400), 6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('queue preview remains safe for unusually short windows', () => {
|
|
||||||
assert.equal(queuePreviewRowCount(0), 1);
|
|
||||||
assert.equal(queuePreviewRowCount(320), 1);
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export const QUEUE_ROW_HEIGHT = 64;
|
|
||||||
export const QUEUE_INITIAL_RENDER_AHEAD_ROWS = 4;
|
|
||||||
export const QUEUE_INITIAL_RENDER_DISTANCE =
|
|
||||||
QUEUE_ROW_HEIGHT * QUEUE_INITIAL_RENDER_AHEAD_ROWS;
|
|
||||||
export const QUEUE_RENDER_AHEAD_MIN_ROWS = 12;
|
|
||||||
export const QUEUE_RENDER_WINDOW_MULTIPLIER = 2;
|
|
||||||
|
|
||||||
const QUEUE_SHEET_INITIAL_FRACTION = 0.58;
|
|
||||||
const QUEUE_PREVIEW_NON_LIST_HEIGHT = 220;
|
|
||||||
const QUEUE_PREVIEW_MAX_ROWS = 6;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The preview only fills the list portion of the initial sheet snap. Using the
|
|
||||||
* whole window height used to duplicate far more rows than could be visible
|
|
||||||
* while FlashList was mounting its own render-ahead window underneath.
|
|
||||||
*/
|
|
||||||
export function queuePreviewRowCount(windowHeight: number): number {
|
|
||||||
const initialListHeight = Math.max(
|
|
||||||
QUEUE_ROW_HEIGHT,
|
|
||||||
windowHeight * QUEUE_SHEET_INITIAL_FRACTION - QUEUE_PREVIEW_NON_LIST_HEIGHT
|
|
||||||
);
|
|
||||||
return Math.min(
|
|
||||||
QUEUE_PREVIEW_MAX_ROWS,
|
|
||||||
Math.max(1, Math.ceil(initialListHeight / QUEUE_ROW_HEIGHT))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queueRenderDistance(windowHeight: number): number {
|
|
||||||
const safeWindowHeight = Number.isFinite(windowHeight)
|
|
||||||
? Math.max(0, windowHeight)
|
|
||||||
: 0;
|
|
||||||
return Math.max(
|
|
||||||
QUEUE_ROW_HEIGHT * QUEUE_RENDER_AHEAD_MIN_ROWS,
|
|
||||||
Math.ceil(safeWindowHeight * QUEUE_RENDER_WINDOW_MULTIPLIER),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Event, useTrackPlayerEvents, type Track as RntpTrack } from 'react-native-track-player';
|
|
||||||
import { useQueueStore } from '@/stores/queueStore';
|
|
||||||
import { usePlayerStore } from '@/stores/playerStore';
|
|
||||||
import { nativeIndexToAbsolute } from '@/audio/queueLoader';
|
|
||||||
|
|
||||||
export interface QueueSnapshot {
|
|
||||||
tracks: RntpTrack[];
|
|
||||||
activeIndex: number;
|
|
||||||
hasSnapshot: boolean;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Live view of the JS queue mirror. Opening the tray only falls back to RNTP's
|
|
||||||
* full native queue read when no playback action has populated the mirror yet.
|
|
||||||
*/
|
|
||||||
export function useQueue(active: boolean): QueueSnapshot {
|
|
||||||
const tracks = useQueueStore((s) => s.tracks);
|
|
||||||
const activeIndex = useQueueStore((s) => s.activeIndex);
|
|
||||||
const hasSnapshot = useQueueStore((s) => s.hasSnapshot);
|
|
||||||
const refresh = useQueueStore((s) => s.refreshFromNative);
|
|
||||||
const refreshActiveIndex = useQueueStore((s) => s.refreshActiveIndex);
|
|
||||||
const setActiveIndex = useQueueStore((s) => s.setActiveIndex);
|
|
||||||
const restoredSessionPending = usePlayerStore((s) => s.restoredSessionPending);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!active) return;
|
|
||||||
if (restoredSessionPending) return;
|
|
||||||
if (hasSnapshot) void refreshActiveIndex();
|
|
||||||
else void refresh();
|
|
||||||
}, [active, hasSnapshot, refresh, refreshActiveIndex, restoredSessionPending]);
|
|
||||||
|
|
||||||
useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], (event) => {
|
|
||||||
if (!active) return;
|
|
||||||
if (restoredSessionPending) return;
|
|
||||||
if (hasSnapshot) {
|
|
||||||
// Event indices are native — shifted while a chunked load is prepending the head.
|
|
||||||
setActiveIndex(event.index != null ? nativeIndexToAbsolute(event.index) : -1);
|
|
||||||
} else {
|
|
||||||
void refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { tracks, activeIndex, hasSnapshot, refresh };
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import test from 'node:test';
|
|
||||||
import {
|
|
||||||
VIRTUAL_QUEUE_PAGE_SIZE,
|
|
||||||
VIRTUAL_QUEUE_PREFETCH_ROWS,
|
|
||||||
isCurrentVirtualQueueRequest,
|
|
||||||
isVirtualQueueWaitingForTracks,
|
|
||||||
mergeVirtualQueueTracks,
|
|
||||||
nextVirtualQueuePageStart,
|
|
||||||
seedVirtualQueueTracks,
|
|
||||||
shouldPrefetchVirtualQueue,
|
|
||||||
} from './virtualQueuePaging.ts';
|
|
||||||
|
|
||||||
interface TestTrack {
|
|
||||||
id: string;
|
|
||||||
astraQueuePosition: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tracks(start: number, count: number): TestTrack[] {
|
|
||||||
return Array.from({ length: count }, (_, index) => ({
|
|
||||||
id: `track-${start + index}`,
|
|
||||||
astraQueuePosition: start + index,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
test('seeds the tray from the already-loaded playback window after the active row', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
seedVirtualQueueTracks(tracks(20, 8), 22, 27).map((track) => track.astraQueuePosition),
|
|
||||||
[23, 24, 25, 26],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('merges sequential virtual pages without dropping earlier tray rows', () => {
|
|
||||||
const first = tracks(1, VIRTUAL_QUEUE_PAGE_SIZE);
|
|
||||||
const second = tracks(VIRTUAL_QUEUE_PAGE_SIZE + 1, VIRTUAL_QUEUE_PAGE_SIZE);
|
|
||||||
const third = tracks(VIRTUAL_QUEUE_PAGE_SIZE * 2 + 1, VIRTUAL_QUEUE_PAGE_SIZE);
|
|
||||||
const merged = mergeVirtualQueueTracks(
|
|
||||||
mergeVirtualQueueTracks(first, second),
|
|
||||||
third,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.equal(merged.length, VIRTUAL_QUEUE_PAGE_SIZE * 3);
|
|
||||||
assert.equal(merged[0], first[0]);
|
|
||||||
assert.equal(merged.at(-1), third.at(-1));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('deduplicates overlapping page boundaries and keeps existing row identity', () => {
|
|
||||||
const first = tracks(1, 3);
|
|
||||||
const duplicate = { id: 'replacement', astraQueuePosition: 3 };
|
|
||||||
const merged = mergeVirtualQueueTracks(first, [duplicate, ...tracks(4, 2)]);
|
|
||||||
|
|
||||||
assert.deepEqual(merged.map((track) => track.astraQueuePosition), [1, 2, 3, 4, 5]);
|
|
||||||
assert.equal(merged[2], first[2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('prefetches one hundred rows before the loaded end', () => {
|
|
||||||
assert.equal(VIRTUAL_QUEUE_PREFETCH_ROWS, 100);
|
|
||||||
assert.equal(shouldPrefetchVirtualQueue(399, 500, true), false);
|
|
||||||
assert.equal(shouldPrefetchVirtualQueue(400, 500, true), true);
|
|
||||||
assert.equal(shouldPrefetchVirtualQueue(499, 500, false), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('stops paging at the virtual queue end', () => {
|
|
||||||
assert.equal(nextVirtualQueuePageStart(tracks(1, 4), 0, 6), 5);
|
|
||||||
assert.equal(nextVirtualQueuePageStart(tracks(1, 5), 0, 6), null);
|
|
||||||
assert.equal(nextVirtualQueuePageStart([], 5, 6), null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loads an empty virtual seed immediately when tracks remain', () => {
|
|
||||||
assert.equal(isVirtualQueueWaitingForTracks(0, 5, 10), true);
|
|
||||||
assert.equal(isVirtualQueueWaitingForTracks(1, 5, 10), false);
|
|
||||||
assert.equal(isVirtualQueueWaitingForTracks(0, 9, 10), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rejects stale generations and replaced playback sessions', () => {
|
|
||||||
assert.equal(isCurrentVirtualQueueRequest(4, 4, 'session-a', 'session-a'), true);
|
|
||||||
assert.equal(isCurrentVirtualQueueRequest(3, 4, 'session-a', 'session-a'), false);
|
|
||||||
assert.equal(isCurrentVirtualQueueRequest(4, 4, 'session-a', 'session-b'), false);
|
|
||||||
assert.equal(isCurrentVirtualQueueRequest(4, 4, 'session-a', undefined), false);
|
|
||||||
});
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
export const VIRTUAL_QUEUE_PAGE_SIZE = 250;
|
|
||||||
export const VIRTUAL_QUEUE_PREFETCH_ROWS = 100;
|
|
||||||
|
|
||||||
interface VirtualPositionedTrack {
|
|
||||||
astraQueuePosition?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function virtualQueuePosition(track: unknown): number | null {
|
|
||||||
const position = (track as VirtualPositionedTrack | null)?.astraQueuePosition;
|
|
||||||
return typeof position === 'number' && Number.isFinite(position) && position >= 0
|
|
||||||
? position
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps every page loaded during the current tray session. Existing rows win
|
|
||||||
* at duplicate page boundaries so memoized queue rows retain their identity.
|
|
||||||
*/
|
|
||||||
export function mergeVirtualQueueTracks<T>(
|
|
||||||
existing: readonly T[],
|
|
||||||
incoming: readonly T[],
|
|
||||||
): T[] {
|
|
||||||
const tracksByPosition = new Map<number, T>();
|
|
||||||
|
|
||||||
existing.forEach((track) => {
|
|
||||||
const position = virtualQueuePosition(track);
|
|
||||||
if (position !== null) tracksByPosition.set(position, track);
|
|
||||||
});
|
|
||||||
incoming.forEach((track) => {
|
|
||||||
const position = virtualQueuePosition(track);
|
|
||||||
if (position !== null && !tracksByPosition.has(position)) {
|
|
||||||
tracksByPosition.set(position, track);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...tracksByPosition.entries()]
|
|
||||||
.sort(([left], [right]) => left - right)
|
|
||||||
.map(([, track]) => track);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seedVirtualQueueTracks<T>(
|
|
||||||
playbackWindow: readonly T[],
|
|
||||||
activePosition: number,
|
|
||||||
totalCount: number,
|
|
||||||
): T[] {
|
|
||||||
return mergeVirtualQueueTracks(
|
|
||||||
[],
|
|
||||||
playbackWindow.filter((track) => {
|
|
||||||
const position = virtualQueuePosition(track);
|
|
||||||
return position !== null && position > activePosition && position < totalCount;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nextVirtualQueuePageStart(
|
|
||||||
tracks: readonly unknown[],
|
|
||||||
activePosition: number,
|
|
||||||
totalCount: number,
|
|
||||||
): number | null {
|
|
||||||
const lastPosition = tracks.length > 0
|
|
||||||
? virtualQueuePosition(tracks[tracks.length - 1])
|
|
||||||
: null;
|
|
||||||
const start = (lastPosition ?? activePosition) + 1;
|
|
||||||
return start < totalCount ? start : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Break the empty-list mount deadlock: when the playback window has no usable
|
|
||||||
* upcoming rows but the virtual context still has tracks, paging cannot wait
|
|
||||||
* for FlashList's first paint because an empty list may never report one.
|
|
||||||
*/
|
|
||||||
export function isVirtualQueueWaitingForTracks(
|
|
||||||
visibleTrackCount: number,
|
|
||||||
activePosition: number,
|
|
||||||
totalCount: number,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
visibleTrackCount === 0
|
|
||||||
&& nextVirtualQueuePageStart([], activePosition, totalCount) !== null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldPrefetchVirtualQueue(
|
|
||||||
lastVisibleIndex: number,
|
|
||||||
loadedCount: number,
|
|
||||||
hasMore: boolean,
|
|
||||||
prefetchRows = VIRTUAL_QUEUE_PREFETCH_ROWS,
|
|
||||||
): boolean {
|
|
||||||
if (!hasMore || loadedCount <= 0 || lastVisibleIndex < 0) return false;
|
|
||||||
return lastVisibleIndex >= Math.max(0, loadedCount - prefetchRows);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCurrentVirtualQueueRequest(
|
|
||||||
requestGeneration: number,
|
|
||||||
currentGeneration: number,
|
|
||||||
requestSessionId: string,
|
|
||||||
currentSessionId: string | undefined,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
requestGeneration === currentGeneration &&
|
|
||||||
requestSessionId === currentSessionId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ const LYRICS_VISIBLE_KEY = 'lyrics_visible';
|
|||||||
const NOW_PLAYING_COMPANION_KEY = 'now_playing_companion';
|
const NOW_PLAYING_COMPANION_KEY = 'now_playing_companion';
|
||||||
const HOME_GREETING_TEXT_MODE_KEY = 'home_greeting_text_mode';
|
const HOME_GREETING_TEXT_MODE_KEY = 'home_greeting_text_mode';
|
||||||
const LISTENING_HISTORY_ENABLED_KEY = 'listening_history_enabled';
|
const LISTENING_HISTORY_ENABLED_KEY = 'listening_history_enabled';
|
||||||
const NATIVE_QUEUE_ENABLED_KEY = 'native_queue_enabled';
|
|
||||||
|
|
||||||
/** Which visualizer the now-playing scope stage shows. */
|
/** Which visualizer the now-playing scope stage shows. */
|
||||||
export type ScopeMode = 'spectrum' | 'scope';
|
export type ScopeMode = 'spectrum' | 'scope';
|
||||||
@@ -69,7 +68,6 @@ interface SettingsStore {
|
|||||||
nowPlayingCompanion: NowPlayingCompanion;
|
nowPlayingCompanion: NowPlayingCompanion;
|
||||||
homeGreetingTextMode: HomeGreetingTextMode;
|
homeGreetingTextMode: HomeGreetingTextMode;
|
||||||
listeningHistoryEnabled: boolean;
|
listeningHistoryEnabled: boolean;
|
||||||
nativeQueueEnabled: boolean;
|
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
load: () => Promise<void>;
|
load: () => Promise<void>;
|
||||||
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
|
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
|
||||||
@@ -81,7 +79,6 @@ interface SettingsStore {
|
|||||||
setNowPlayingCompanion: (companion: NowPlayingCompanion) => Promise<void>;
|
setNowPlayingCompanion: (companion: NowPlayingCompanion) => Promise<void>;
|
||||||
setHomeGreetingTextMode: (mode: HomeGreetingTextMode) => Promise<void>;
|
setHomeGreetingTextMode: (mode: HomeGreetingTextMode) => Promise<void>;
|
||||||
setListeningHistoryEnabled: (enabled: boolean) => Promise<void>;
|
setListeningHistoryEnabled: (enabled: boolean) => Promise<void>;
|
||||||
setNativeQueueEnabled: (enabled: boolean) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||||
@@ -94,7 +91,6 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
nowPlayingCompanion: 'queue',
|
nowPlayingCompanion: 'queue',
|
||||||
homeGreetingTextMode: 'messages',
|
homeGreetingTextMode: 'messages',
|
||||||
listeningHistoryEnabled: true,
|
listeningHistoryEnabled: true,
|
||||||
nativeQueueEnabled: false,
|
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
|
||||||
load: async () => {
|
load: async () => {
|
||||||
@@ -110,7 +106,6 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
NOW_PLAYING_COMPANION_KEY,
|
NOW_PLAYING_COMPANION_KEY,
|
||||||
HOME_GREETING_TEXT_MODE_KEY,
|
HOME_GREETING_TEXT_MODE_KEY,
|
||||||
LISTENING_HISTORY_ENABLED_KEY,
|
LISTENING_HISTORY_ENABLED_KEY,
|
||||||
NATIVE_QUEUE_ENABLED_KEY,
|
|
||||||
]);
|
]);
|
||||||
const grouping = values[ARTIST_GROUPING_KEY] ?? null;
|
const grouping = values[ARTIST_GROUPING_KEY] ?? null;
|
||||||
const includeSingles = values[INCLUDE_SINGLES_KEY] ?? null;
|
const includeSingles = values[INCLUDE_SINGLES_KEY] ?? null;
|
||||||
@@ -121,7 +116,6 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
const nowPlayingCompanion = values[NOW_PLAYING_COMPANION_KEY] ?? null;
|
const nowPlayingCompanion = values[NOW_PLAYING_COMPANION_KEY] ?? null;
|
||||||
const homeGreetingTextMode = values[HOME_GREETING_TEXT_MODE_KEY] ?? null;
|
const homeGreetingTextMode = values[HOME_GREETING_TEXT_MODE_KEY] ?? null;
|
||||||
const listeningHistoryEnabled = values[LISTENING_HISTORY_ENABLED_KEY] !== '0';
|
const listeningHistoryEnabled = values[LISTENING_HISTORY_ENABLED_KEY] !== '0';
|
||||||
const nativeQueueEnabled = parseBoolean(values[NATIVE_QUEUE_ENABLED_KEY] ?? null);
|
|
||||||
set({
|
set({
|
||||||
artistGroupingMode: parseGroupingMode(grouping),
|
artistGroupingMode: parseGroupingMode(grouping),
|
||||||
includeSingles: parseBoolean(includeSingles),
|
includeSingles: parseBoolean(includeSingles),
|
||||||
@@ -132,7 +126,6 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
nowPlayingCompanion: parseNowPlayingCompanion(nowPlayingCompanion),
|
nowPlayingCompanion: parseNowPlayingCompanion(nowPlayingCompanion),
|
||||||
homeGreetingTextMode: parseHomeGreetingTextMode(homeGreetingTextMode),
|
homeGreetingTextMode: parseHomeGreetingTextMode(homeGreetingTextMode),
|
||||||
listeningHistoryEnabled,
|
listeningHistoryEnabled,
|
||||||
nativeQueueEnabled,
|
|
||||||
loaded: true,
|
loaded: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -201,12 +194,4 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setNativeQueueEnabled: async (enabled) => {
|
|
||||||
if (get().nativeQueueEnabled === enabled) return;
|
|
||||||
set({ nativeQueueEnabled: enabled });
|
|
||||||
await AstraLibraryData.setSettings({
|
|
||||||
[NATIVE_QUEUE_ENABLED_KEY]: enabled ? 'true' : 'false',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user