mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(player): polish sleep-timer UI — circular ring + in-button countdown (#272)
Replaces the text-pill schedule badge with a circular SVG progress ring
around the play/pause button. Accent→lavender gradient stroke, counter-
clockwise depletion synced to the armed deadline, subtle accent glow.
While a timer is armed, the Play/Pause icon inside the button is
swapped for a two-line stack: a small Moon (sleep timer) or Sunrise
(delayed start) glyph above the countdown (m:ss / h:mm:ss). The icon
is the mode marker so the ring can stay unified with the rest of the
app's accent palette.
Redesigns the schedule modal with a mood-tinted header (Moon for
"Pause after", Sunrise for "Start after"), soft radial accent glow in
the background, slide-up open animation. Circular glass close-button
scoped to this modal, with hover rotation + focus ring. Custom-minutes
field gains an inline "min" suffix. A live preview line at the bottom
shows when the action fires ("Pauses at 23:47" / "Starts at 23:47"),
updating as the user hovers a preset or types. Chips gain a small
hover lift plus accent-coloured shadow.
Also fixes a pre-existing duplicate-tooltip on the play/pause button:
title= attributes removed alongside the data-tooltip (title violates
the project's "never native tooltips" rule, so both showed at once).
Adds scheduledPauseStartMs / scheduledResumeStartMs to the player
store so the progress ring has a total-duration baseline, set and
cleared alongside the existing deadline fields.
New usePlaybackScheduleRemaining hook wraps the store subscription
and 500 ms tick into a single hook used by all three player views
(PlayerBar / FullscreenPlayer / MobilePlayerView).
i18n: delayPreviewPause / delayPreviewStart keys across all 8 locales.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
624ce56faf
commit
694567843f
+22
-12
@@ -177,8 +177,12 @@ interface PlayerState {
|
||||
togglePlay: () => void;
|
||||
/** Wall-clock ms when auto-pause fires, or null. */
|
||||
scheduledPauseAtMs: number | null;
|
||||
/** Wall-clock ms when the current auto-pause timer was armed (for progress-ring totals). */
|
||||
scheduledPauseStartMs: number | null;
|
||||
/** Wall-clock ms when auto-resume fires, or null. */
|
||||
scheduledResumeAtMs: number | null;
|
||||
/** Wall-clock ms when the current auto-resume timer was armed (for progress-ring totals). */
|
||||
scheduledResumeStartMs: number | null;
|
||||
schedulePauseIn: (seconds: number) => void;
|
||||
scheduleResumeIn: (seconds: number) => void;
|
||||
clearScheduledPause: () => void;
|
||||
@@ -1051,7 +1055,9 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
isQueueVisible: true,
|
||||
isFullscreenOpen: false,
|
||||
scheduledPauseAtMs: null,
|
||||
scheduledPauseStartMs: null,
|
||||
scheduledResumeAtMs: null,
|
||||
scheduledResumeStartMs: null,
|
||||
repeatMode: 'off',
|
||||
contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null },
|
||||
|
||||
@@ -1147,7 +1153,9 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
currentPlaybackSource: null,
|
||||
enginePreloadedTrackId: null,
|
||||
scheduledPauseAtMs: null,
|
||||
scheduledPauseStartMs: null,
|
||||
scheduledResumeAtMs: null,
|
||||
scheduledResumeStartMs: null,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1156,7 +1164,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
const { volume } = get();
|
||||
++playGeneration;
|
||||
clearAllPlaybackScheduleTimers();
|
||||
set({ scheduledPauseAtMs: null, scheduledResumeAtMs: null });
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
isAudioPaused = false;
|
||||
clearRadioReconnectTimer();
|
||||
radioReconnectCount = 0;
|
||||
@@ -1202,7 +1210,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
}
|
||||
|
||||
clearAllPlaybackScheduleTimers();
|
||||
set({ scheduledPauseAtMs: null, scheduledResumeAtMs: null });
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
|
||||
const gen = ++playGeneration;
|
||||
isAudioPaused = false;
|
||||
@@ -1363,7 +1371,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
invoke('audio_pause').catch(console.error);
|
||||
isAudioPaused = true;
|
||||
}
|
||||
set({ isPlaying: false, scheduledPauseAtMs: null, scheduledResumeAtMs: null });
|
||||
set({ isPlaying: false, scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
},
|
||||
|
||||
resetAudioPause: () => {
|
||||
@@ -1372,7 +1380,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
|
||||
resume: () => {
|
||||
clearAllPlaybackScheduleTimers();
|
||||
set({ scheduledPauseAtMs: null, scheduledResumeAtMs: null });
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null, scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
if (get().currentRadio) {
|
||||
radioAudio.play().catch(console.error);
|
||||
set({ isPlaying: true });
|
||||
@@ -1467,12 +1475,12 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
|
||||
clearScheduledPause: () => {
|
||||
clearScheduledPauseTimers();
|
||||
set({ scheduledPauseAtMs: null });
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null });
|
||||
},
|
||||
|
||||
clearScheduledResume: () => {
|
||||
clearScheduledResumeTimers();
|
||||
set({ scheduledResumeAtMs: null });
|
||||
set({ scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
},
|
||||
|
||||
schedulePauseIn: (seconds) => {
|
||||
@@ -1480,11 +1488,12 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
if (!s.isPlaying) return;
|
||||
clearScheduledPauseTimers();
|
||||
const delayMs = Math.max(500, Math.round(Number(seconds) * 1000));
|
||||
const at = Date.now() + delayMs;
|
||||
set({ scheduledPauseAtMs: at });
|
||||
const startedAt = Date.now();
|
||||
const at = startedAt + delayMs;
|
||||
set({ scheduledPauseAtMs: at, scheduledPauseStartMs: startedAt });
|
||||
scheduledPauseTimer = window.setTimeout(() => {
|
||||
scheduledPauseTimer = null;
|
||||
set({ scheduledPauseAtMs: null });
|
||||
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null });
|
||||
get().pause();
|
||||
}, delayMs) as unknown as number;
|
||||
},
|
||||
@@ -1495,11 +1504,12 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
if (!s.currentTrack && !s.currentRadio) return;
|
||||
clearScheduledResumeTimers();
|
||||
const delayMs = Math.max(500, Math.round(Number(seconds) * 1000));
|
||||
const at = Date.now() + delayMs;
|
||||
set({ scheduledResumeAtMs: at });
|
||||
const startedAt = Date.now();
|
||||
const at = startedAt + delayMs;
|
||||
set({ scheduledResumeAtMs: at, scheduledResumeStartMs: startedAt });
|
||||
scheduledResumeTimer = window.setTimeout(() => {
|
||||
scheduledResumeTimer = null;
|
||||
set({ scheduledResumeAtMs: null });
|
||||
set({ scheduledResumeAtMs: null, scheduledResumeStartMs: null });
|
||||
get().resume();
|
||||
}, delayMs) as unknown as number;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user