feat: optimize volume input control
This commit is contained in:
1 parent
11a56cf44e
commit
6b1256cfb2
4 files changed
+94
-20
No files matched your search
@@ -13,13 +13,18 @@
|
||||
.progress-track { height: 3px; flex: 1; background: var(--color-border-hover); border-radius: 4px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: var(--color-foreground); transform: scaleX(0); transform-origin: left; }
|
||||
.player-tools { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
||||
.player-tools .volume-slider { width: 90px; margin-right: 16px; accent-color: var(--color-foreground); }
|
||||
.player-tools .volume-control { display: flex; align-items: center; gap: 6px; margin-right: 16px; }
|
||||
.player-tools .volume-slider { width: 90px; accent-color: var(--color-foreground); }
|
||||
.player-tools .volume-input { box-sizing: border-box; width: 4.5ch; min-width: 0; padding: 4px 3px; text-align: center; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
|
||||
.player-tools .volume-input::-webkit-inner-spin-button, .player-tools .volume-input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
|
||||
.player-tools .volume-unit { color: var(--color-foreground-subtle); font-size: var(--text-ui-xs); }
|
||||
.player-panel-toggle { color: var(--color-foreground-subtle); }
|
||||
.player-panel-toggle:hover:not(:disabled), .player-panel-toggle:focus-visible:not(:disabled), .player-panel-toggle[aria-pressed='true'] { color: var(--color-brand); background: transparent; }
|
||||
.player-panel-toggle:active:not(:disabled) { color: var(--color-brand); background: transparent; transform: none; }
|
||||
@media (max-width: 1100px) {
|
||||
.player-bar { gap: 12px; }
|
||||
.player-tools .volume-slider { width: 60px; margin-right: 4px; }
|
||||
.player-tools .volume-control { margin-right: 4px; }
|
||||
.player-tools .volume-slider { width: 60px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
/* 窄屏的播放器栏由内容撑开高度;内容比封面矮时(比如出错提示),仍保持封面加上下内边距的高度。 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ListMusic, MessageCircle, SkipForward, Volume2, VolumeX } from 'lucide-react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { roomStore } from '@/core/room'
|
||||
import { sourceName } from '@/features/queue'
|
||||
import { bookTitle } from '@/shared/lib/book-title'
|
||||
@@ -65,7 +65,26 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
|
||||
const connected = useStore(roomStore, s => s.status === 'open')
|
||||
const volume = useStore(volumeStore, s => s.volume)
|
||||
const muted = useStore(volumeStore, s => s.muted)
|
||||
const [volumeDraft, setVolumeDraft] = useState(String(volume))
|
||||
const volumeEditingRef = useRef(false)
|
||||
const meta = nowPlaying?.entry.meta
|
||||
useEffect(() => {
|
||||
if (!volumeEditingRef.current)
|
||||
setVolumeDraft(String(volume))
|
||||
}, [volume])
|
||||
const commitVolumeDraft = () => {
|
||||
const parsed = Number(volumeDraft)
|
||||
if (volumeDraft.trim() === '' || !Number.isFinite(parsed)) {
|
||||
setVolumeDraft(String(volume))
|
||||
return
|
||||
}
|
||||
const next = Math.min(100, Math.max(0, Math.round(parsed)))
|
||||
volumeStore.setVolume(next)
|
||||
setVolumeDraft(String(next))
|
||||
if (muted && next > 0)
|
||||
volumeStore.setMuted(false)
|
||||
}
|
||||
|
||||
const source = nowPlaying && sourceName(nowPlaying.entry.track)
|
||||
return (
|
||||
<section className="player-bar" aria-label="播放器">
|
||||
@@ -95,20 +114,49 @@ export function PlayerBar({ onSkip, onLyrics, onQueue, onChat, queueOpen, chatOp
|
||||
</div>
|
||||
<div className="player-tools">
|
||||
<IconButton icon={muted ? VolumeX : Volume2} label={muted ? '取消静音' : '静音'} onClick={() => volumeStore.setMuted(!muted)} />
|
||||
<input
|
||||
className="volume-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={muted ? 0 : volume}
|
||||
aria-label="音量"
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value)
|
||||
volumeStore.setVolume(next)
|
||||
if (muted && next > 0)
|
||||
volumeStore.setMuted(false)
|
||||
}}
|
||||
/>
|
||||
<div className="volume-control">
|
||||
<input
|
||||
className="volume-slider"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={muted ? 0 : volume}
|
||||
aria-label="音量"
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value)
|
||||
volumeStore.setVolume(next)
|
||||
setVolumeDraft(String(next))
|
||||
if (muted && next > 0)
|
||||
volumeStore.setMuted(false)
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="volume-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
inputMode="numeric"
|
||||
value={volumeDraft}
|
||||
aria-label="音量百分比"
|
||||
onFocus={() => {
|
||||
volumeEditingRef.current = true
|
||||
}}
|
||||
onChange={event => setVolumeDraft(event.target.value)}
|
||||
onBlur={() => {
|
||||
volumeEditingRef.current = false
|
||||
commitVolumeDraft()
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter')
|
||||
return
|
||||
event.preventDefault()
|
||||
commitVolumeDraft()
|
||||
event.currentTarget.blur()
|
||||
}}
|
||||
/>
|
||||
<span className="volume-unit" aria-hidden="true">%</span>
|
||||
</div>
|
||||
<IconButton icon={MessageCircle} label="房间聊天" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={chatOpen} onClick={onChat} />
|
||||
<IconButton icon={ListMusic} label="播放列表" className="player-panel-toggle" disabled={!canOpenPanels} aria-pressed={queueOpen} onClick={onQueue} />
|
||||
</div>
|
||||
|
||||
@@ -209,8 +209,25 @@ export function SettingsPage() {
|
||||
<div className="settings-group">
|
||||
<SettingRow id="settings-volume" title="音量" description="只调整这台设备的播放音量。" field>
|
||||
<div className="settings-volume-control">
|
||||
<input id="settings-volume" aria-describedby="settings-volume-description" type="range" min="0" max="100" value={draft.volume} onChange={event => update('volume', Number(event.target.value))} />
|
||||
<output htmlFor="settings-volume" className="text-ui-caption muted">{`${draft.volume}%`}</output>
|
||||
<input id="settings-volume" aria-describedby="settings-volume-description" type="range" min="0" max="100" value={draft.volume} aria-label="音量滑块" onChange={event => update('volume', Number(event.target.value))} />
|
||||
<div className="settings-volume-input-control">
|
||||
<input
|
||||
className="settings-volume-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
inputMode="numeric"
|
||||
value={draft.volume}
|
||||
aria-label="音量百分比"
|
||||
onChange={(event) => {
|
||||
const value = Number(event.target.value)
|
||||
if (Number.isFinite(value))
|
||||
update('volume', Math.min(100, Math.max(0, Math.round(value))))
|
||||
}}
|
||||
/>
|
||||
<span className="settings-volume-unit" aria-hidden="true">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</SettingRow>
|
||||
<SettingRow id="settings-muted" title="静音" description="暂时关闭这台设备的声音。">
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
.settings-row-control select:disabled, .settings-row-control input:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.settings-row-field .settings-row-control { width: 280px; }
|
||||
.settings-volume-control { display: flex; align-items: center; gap: 16px; width: 100%; }
|
||||
.settings-volume-control output { min-width: 4ch; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.settings-volume-input-control { display: flex; align-items: center; gap: 4px; }
|
||||
.settings-volume-input { -moz-appearance: textfield; }
|
||||
.settings-volume-input::-webkit-inner-spin-button, .settings-volume-input::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
|
||||
.settings-volume-input { box-sizing: border-box; width: 5ch; padding: 6px 4px; text-align: center; font-variant-numeric: tabular-nums; }
|
||||
.settings-volume-unit { color: var(--color-foreground-subtle); font-size: var(--text-ui-xs); }
|
||||
.settings-switch[type='checkbox'] { appearance: none; position: relative; width: 40px; height: 24px; margin: 0; border: 1px solid var(--color-border-hover); border-radius: 999px; background: var(--color-selected); cursor: pointer; }
|
||||
.settings-switch[type='checkbox']::before { content: ''; display: block; width: 18px; height: 18px; margin: 2px; border-radius: 50%; background: var(--color-foreground); transform: translateX(0); transition: transform 150ms ease; }
|
||||
.settings-switch[type='checkbox']:checked { border-color: var(--color-primary); background: var(--color-primary); }
|
||||
|
||||
Reference in new issue
Block a user