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
@@ -1,3 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
|
||||
/** Remaining time until wall-clock `deadlineMs` (m:ss or h:mm:ss). */
|
||||
export function formatPlaybackScheduleRemaining(deadlineMs: number | null, nowMs: number): string {
|
||||
if (deadlineMs == null) return '';
|
||||
@@ -11,3 +15,39 @@ export function formatPlaybackScheduleRemaining(deadlineMs: number | null, nowMs
|
||||
}
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export interface PlaybackScheduleInfo {
|
||||
/** Formatted countdown, e.g. "4:32". */
|
||||
remaining: string;
|
||||
/** Which timer is running: `pause` = sleep-timer, `start` = delayed-start. */
|
||||
mode: 'pause' | 'start';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: returns the active playback-schedule countdown + its mode, or null
|
||||
* when no timer is armed. Ticks every 500 ms so the caller never needs to
|
||||
* set up its own interval. Used by the play/pause button to swap the
|
||||
* Play/Pause icon for a mode-icon + countdown text while a timer runs.
|
||||
*/
|
||||
export function usePlaybackScheduleRemaining(): PlaybackScheduleInfo | null {
|
||||
const { isPlaying, scheduledPauseAtMs, scheduledResumeAtMs } = usePlayerStore(
|
||||
useShallow(s => ({
|
||||
isPlaying: s.isPlaying,
|
||||
scheduledPauseAtMs: s.scheduledPauseAtMs,
|
||||
scheduledResumeAtMs: s.scheduledResumeAtMs,
|
||||
})),
|
||||
);
|
||||
const mode: 'pause' | 'start' | null =
|
||||
isPlaying && scheduledPauseAtMs != null ? 'pause'
|
||||
: !isPlaying && scheduledResumeAtMs != null ? 'start'
|
||||
: null;
|
||||
const deadlineMs = mode === 'pause' ? scheduledPauseAtMs : mode === 'start' ? scheduledResumeAtMs : null;
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
if (deadlineMs == null) return;
|
||||
const id = window.setInterval(() => setNowMs(Date.now()), 500);
|
||||
return () => window.clearInterval(id);
|
||||
}, [deadlineMs]);
|
||||
if (mode == null || deadlineMs == null) return null;
|
||||
return { remaining: formatPlaybackScheduleRemaining(deadlineMs, nowMs), mode };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user