調整比賽結算長按回饋並更新 README

This commit is contained in:
2026-04-16 20:06:26 +08:00
parent 975732017f
commit 36a39f0b8f
3 changed files with 151 additions and 3 deletions

View File

@@ -98,6 +98,7 @@ export function ScoreboardPage({
onSwapTeamPlayers,
onUndoLastPoint,
}: ScoreboardPageProps) {
const FINISH_HOLD_DURATION = 1500
const [pickerOpen, setPickerOpen] = useState(false)
const [settingsOpen, setSettingsOpen] = useState(false)
const [draftPlayers, setDraftPlayers] = useState<string[]>([])
@@ -105,9 +106,14 @@ export function ScoreboardPage({
String(scoreState.targetScore),
)
const [clock, setClock] = useState(() => formatClock())
const [finishHoldActive, setFinishHoldActive] = useState(false)
const [finishHoldProgress, setFinishHoldProgress] = useState(0)
const [voiceSettings, setVoiceSettings] = useState<VoiceSettings>(() =>
loadVoiceSettings(),
)
const finishHoldFrameRef = useRef<number | null>(null)
const finishHoldStartRef = useRef(0)
const finishTriggeredRef = useRef(false)
const lastAnnouncedPointRef = useRef(0)
const previousScoresRef = useRef({ left: 0, right: 0 })
@@ -128,6 +134,10 @@ export function ScoreboardPage({
useEffect(() => {
return () => {
if (finishHoldFrameRef.current !== null) {
window.cancelAnimationFrame(finishHoldFrameRef.current)
}
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel()
}
@@ -301,6 +311,57 @@ export function ScoreboardPage({
setPickerOpen(true)
}
const stopFinishHold = () => {
if (finishHoldFrameRef.current !== null) {
window.cancelAnimationFrame(finishHoldFrameRef.current)
finishHoldFrameRef.current = null
}
finishHoldStartRef.current = 0
finishTriggeredRef.current = false
setFinishHoldActive(false)
setFinishHoldProgress(0)
}
const startFinishHold = () => {
if (finishDialogOpen || finishDialogUploading || finishHoldActive) {
return
}
finishTriggeredRef.current = false
finishHoldStartRef.current = performance.now()
setFinishHoldActive(true)
setFinishHoldProgress(0)
const tick = (now: number) => {
const elapsed = now - finishHoldStartRef.current
const progress = Math.min(elapsed / FINISH_HOLD_DURATION, 1)
setFinishHoldProgress(progress)
if (progress >= 1) {
finishTriggeredRef.current = true
setFinishHoldActive(false)
setFinishHoldProgress(0)
finishHoldFrameRef.current = null
onOpenFinishDialog()
return
}
finishHoldFrameRef.current = window.requestAnimationFrame(tick)
}
finishHoldFrameRef.current = window.requestAnimationFrame(tick)
}
const cancelFinishHold = () => {
if (finishTriggeredRef.current) {
finishTriggeredRef.current = false
return
}
stopFinishHold()
}
const toggleDraftPlayer = (playerName: string) => {
setDraftPlayers((current) => {
if (current.includes(playerName)) {
@@ -461,9 +522,48 @@ export function ScoreboardPage({
<div className="rail-clock">{clock}</div>
<button className="rail-pill rail-pill-danger" type="button" onClick={onOpenFinishDialog}>
</button>
<div
className={
finishHoldActive ? 'rail-pill-hold-wrap rail-pill-hold-wrap-active' : 'rail-pill-hold-wrap'
}
>
<button
className={
finishHoldActive
? 'rail-pill rail-pill-danger rail-pill-active-hold'
: 'rail-pill rail-pill-danger'
}
type="button"
onPointerDown={startFinishHold}
onPointerUp={cancelFinishHold}
onPointerLeave={cancelFinishHold}
onPointerCancel={cancelFinishHold}
onBlur={cancelFinishHold}
onKeyDown={(event) => {
if ((event.key === 'Enter' || event.key === ' ') && !event.repeat) {
event.preventDefault()
startFinishHold()
}
}}
onKeyUp={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
cancelFinishHold()
}
}}
>
</button>
{finishHoldActive ? (
<div aria-hidden="true" className="rail-hold-progress">
<span
className="rail-hold-progress-bar"
style={{ transform: `scaleX(${finishHoldProgress})` }}
/>
</div>
) : null}
</div>
</aside>
</section>