mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 13:09:46 +02:00
play on next when paused
This commit is contained in:
+2
-2
@@ -65,7 +65,7 @@
|
||||
"web": "expo start --web",
|
||||
"lint": "expo lint",
|
||||
"test:queue-actions": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/queue/queueActions.test.mts src/components/queue/queuePerformance.test.mts src/components/queue/virtualQueuePaging.test.mts src/components/swipeableRowState.test.mts",
|
||||
"test:desktop-remote": "node --experimental-strip-types --test src/services/desktopRemotePairing.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: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:artist-grouping": "node --experimental-strip-types --test src/library/artistGrouping.test.mts src/audio/artistCreditTransport.test.mts",
|
||||
@@ -75,7 +75,7 @@
|
||||
"test:signal": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/signalShare.test.mts src/audio/signalShareIntent.test.mts src/audio/signalScanGeometry.test.mts src/audio/signalLocalMatch.test.mts",
|
||||
"test:eq-math": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/audio/eq.test.mts",
|
||||
"test:audio-startup": "node --experimental-strip-types --test src/audio/dspStartupCoordinator.test.mts src/audio/dspStartupGain.test.mts",
|
||||
"test:seek-bar": "node --experimental-strip-types --test src/audio/playbackClock.test.mts src/audio/playbackProgressProjection.test.mts src/components/waveformScrubDetents.test.mts",
|
||||
"test:seek-bar": "node --experimental-strip-types --test src/audio/playbackClock.test.mts src/audio/playbackNavigation.test.mts src/audio/playbackProgressProjection.test.mts src/components/waveformScrubDetents.test.mts",
|
||||
"test:recent-play": "node --experimental-strip-types --test src/audio/recentPlayTracking.test.mts",
|
||||
"test:lyrics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lyrics/parsing.test.mts src/lyrics/presentation.test.mts src/lyrics/displaySettings.test.mts src/lyrics/embedded.test.mts src/lyrics/resolver.test.mts",
|
||||
"test:sleep": "node --experimental-strip-types --test src/audio/sleepTimerState.test.mts",
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
prepareAudioProcessingForPlayback,
|
||||
primePreparedTrackForPlayback,
|
||||
} from './audioProcessingStartup';
|
||||
import { shouldRestartOnPrevious } from './playbackNavigation';
|
||||
import {
|
||||
shouldRestartOnPrevious,
|
||||
shouldResumeAfterExplicitNext,
|
||||
} from './playbackNavigation';
|
||||
import {
|
||||
cancelManualRecentPlayTransition,
|
||||
markManualRecentPlayTransition,
|
||||
@@ -815,15 +818,19 @@ export async function togglePlay(): Promise<void> {
|
||||
export async function skipToNext(): Promise<void> {
|
||||
markNowPlayingTrackTransitionDirection('next', 'phone');
|
||||
await ensurePlayerReady();
|
||||
const [nativeQueue, nativeIndex] = await Promise.all([
|
||||
const [nativeQueue, nativeIndex, nativePlaybackState] = await Promise.all([
|
||||
TrackPlayer.getQueue(),
|
||||
TrackPlayer.getActiveTrackIndex(),
|
||||
TrackPlayer.getPlaybackState(),
|
||||
]);
|
||||
await prepareAudioProcessingForPlayback(
|
||||
dspTargetFromTrack(
|
||||
const playbackStateAtIntent = mapRntpState(nativePlaybackState.state);
|
||||
const resumeAfterSkip = shouldResumeAfterExplicitNext(playbackStateAtIntent);
|
||||
const playbackTarget = dspTargetFromTrack(
|
||||
nativeIndex == null ? undefined : nativeQueue[nativeIndex + 1],
|
||||
'none',
|
||||
),
|
||||
);
|
||||
await prepareAudioProcessingForPlayback(
|
||||
playbackTarget,
|
||||
'skip-next',
|
||||
);
|
||||
const { tracks, activeIndex } = useQueueStore.getState();
|
||||
@@ -831,7 +838,10 @@ export async function skipToNext(): Promise<void> {
|
||||
const manualTransitionFromPath = usePlayerStore.getState().currentTrack?.path ?? null;
|
||||
if (nextIndex >= 0 && nextIndex < tracks.length) {
|
||||
useQueueStore.getState().setActiveIndex(nextIndex);
|
||||
setOptimisticTrack(tracks[nextIndex], usePlayerStore.getState().playbackState);
|
||||
setOptimisticTrack(
|
||||
tracks[nextIndex],
|
||||
resumeAfterSkip ? 'loading' : playbackStateAtIntent,
|
||||
);
|
||||
}
|
||||
const manualTransition = markManualRecentPlayTransition(manualTransitionFromPath);
|
||||
let nativeTransitionSucceeded = false;
|
||||
@@ -839,10 +849,15 @@ export async function skipToNext(): Promise<void> {
|
||||
await TrackPlayer.skipToNext();
|
||||
nativeTransitionSucceeded = true;
|
||||
await refreshActiveIndexFromNative();
|
||||
if (resumeAfterSkip) {
|
||||
await primePreparedTrackForPlayback(playbackTarget, 'skip-next-resume');
|
||||
await TrackPlayer.play();
|
||||
usePlayerStore.getState().setPlaybackState('playing');
|
||||
}
|
||||
} catch {
|
||||
if (!nativeTransitionSucceeded) cancelManualRecentPlayTransition(manualTransition);
|
||||
await reconcilePlayerFromNative();
|
||||
// no next track — ignore
|
||||
// No next track or resume failed — keep the reconciled native state.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
PREVIOUS_RESTART_THRESHOLD_SECONDS,
|
||||
shouldResumeAfterExplicitNext,
|
||||
shouldRestartOnPrevious,
|
||||
} from './playbackNavigation.ts';
|
||||
|
||||
@@ -16,3 +17,10 @@ test('invalid and negative positions retain previous-track behavior', () => {
|
||||
assert.equal(shouldRestartOnPrevious(Number.NaN), false);
|
||||
assert.equal(shouldRestartOnPrevious(Number.POSITIVE_INFINITY), false);
|
||||
});
|
||||
|
||||
test('only a paused explicit Next intent resumes playback', () => {
|
||||
assert.equal(shouldResumeAfterExplicitNext('paused'), true);
|
||||
assert.equal(shouldResumeAfterExplicitNext('playing'), false);
|
||||
assert.equal(shouldResumeAfterExplicitNext('loading'), false);
|
||||
assert.equal(shouldResumeAfterExplicitNext('stopped'), false);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PlaybackState } from '@/types/audio';
|
||||
|
||||
export const PREVIOUS_RESTART_THRESHOLD_SECONDS = 3;
|
||||
|
||||
/** Match desktop Astra: restart only after crossing the previous-track cutoff. */
|
||||
@@ -7,3 +9,13 @@ export function shouldRestartOnPrevious(positionSeconds: number): boolean {
|
||||
positionSeconds > PREVIOUS_RESTART_THRESHOLD_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An explicit Next press while paused is also an instruction to resume.
|
||||
* Loading, stopped, and already-playing sessions retain their native behavior.
|
||||
*/
|
||||
export function shouldResumeAfterExplicitNext(
|
||||
playbackState: PlaybackState,
|
||||
): boolean {
|
||||
return playbackState === 'paused';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { desktopRemoteControlSequence } from './desktopRemoteTransport.ts';
|
||||
|
||||
test('paused desktop Next advances before resuming playback', () => {
|
||||
assert.deepEqual(
|
||||
desktopRemoteControlSequence('next', 'paused'),
|
||||
['next', 'play'],
|
||||
);
|
||||
});
|
||||
|
||||
test('desktop Next preserves non-paused transport behavior', () => {
|
||||
assert.deepEqual(desktopRemoteControlSequence('next', 'playing'), ['next']);
|
||||
assert.deepEqual(desktopRemoteControlSequence('next', 'loading'), ['next']);
|
||||
assert.deepEqual(desktopRemoteControlSequence('next', 'stopped'), ['next']);
|
||||
assert.deepEqual(desktopRemoteControlSequence('next', undefined), ['next']);
|
||||
});
|
||||
|
||||
test('other desktop commands are never expanded', () => {
|
||||
assert.deepEqual(desktopRemoteControlSequence('previous', 'paused'), ['previous']);
|
||||
assert.deepEqual(desktopRemoteControlSequence('play', 'paused'), ['play']);
|
||||
assert.deepEqual(desktopRemoteControlSequence('seek', 'paused'), ['seek']);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { shouldResumeAfterExplicitNext } from '../audio/playbackNavigation.ts';
|
||||
import type {
|
||||
DesktopRemoteControlCommand,
|
||||
DesktopRemotePlaybackState,
|
||||
} from '@/types/desktopRemote';
|
||||
|
||||
/**
|
||||
* The desktop protocol exposes discrete transport commands. Expand a paused
|
||||
* Next intent into ordered Next + Play requests so the transition also resumes.
|
||||
*/
|
||||
export function desktopRemoteControlSequence(
|
||||
command: DesktopRemoteControlCommand,
|
||||
playbackState: DesktopRemotePlaybackState | undefined,
|
||||
): DesktopRemoteControlCommand[] {
|
||||
if (
|
||||
command === 'next'
|
||||
&& playbackState
|
||||
&& shouldResumeAfterExplicitNext(playbackState)
|
||||
) {
|
||||
return ['next', 'play'];
|
||||
}
|
||||
return [command];
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { AstraLibraryData } from '../../modules/astra-library-scanner';
|
||||
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
||||
import { identityMatchesPinnedConnection } from '@/services/desktopSyncPolicy';
|
||||
import { ensureDesktopRemoteCredentialsFresh } from '@/services/desktopRemoteSession';
|
||||
import { desktopRemoteControlSequence } from '@/services/desktopRemoteTransport';
|
||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||
import type {
|
||||
DesktopRemoteConnection,
|
||||
@@ -732,16 +733,22 @@ export const useDesktopRemoteStore = create<DesktopRemoteStore>((set, get) => {
|
||||
},
|
||||
|
||||
sendControl: async (command, time) => {
|
||||
const { connection, token } = get();
|
||||
const { connection, token, snapshot } = get();
|
||||
if (!connection || !token) return;
|
||||
try {
|
||||
const commands = desktopRemoteControlSequence(
|
||||
command,
|
||||
snapshot?.playbackState,
|
||||
);
|
||||
for (const nextCommand of commands) {
|
||||
await sendDesktopRemoteControl(
|
||||
connection.baseUrl,
|
||||
token,
|
||||
connection.certificateFingerprint,
|
||||
command,
|
||||
time
|
||||
nextCommand,
|
||||
nextCommand === command ? time : undefined,
|
||||
);
|
||||
}
|
||||
set({ errorMessage: '' });
|
||||
if (command === 'seek' && typeof time === 'number') {
|
||||
set((state) => state.snapshot
|
||||
|
||||
Reference in New Issue
Block a user