調整比賽結算長按回饋並更新 README
This commit is contained in:
46
src/App.css
46
src/App.css
@@ -961,6 +961,15 @@
|
||||
filter 0.16s ease;
|
||||
}
|
||||
|
||||
.rail-pill-hold-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rail-pill-hold-wrap-active {
|
||||
filter: drop-shadow(0 12px 20px rgba(217, 90, 68, 0.2));
|
||||
}
|
||||
|
||||
.rail-pill:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow:
|
||||
@@ -978,11 +987,40 @@
|
||||
background: linear-gradient(180deg, #d95a44, #b53a28);
|
||||
}
|
||||
|
||||
.rail-pill-active-hold {
|
||||
transform: translateY(1px) scale(0.99);
|
||||
filter: brightness(1.08) saturate(1.08);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 227, 214, 0.5),
|
||||
inset 0 12px 18px rgba(255, 255, 255, 0.1),
|
||||
0 0 0 4px rgba(255, 224, 194, 0.2),
|
||||
0 14px 26px rgba(181, 58, 40, 0.28);
|
||||
}
|
||||
|
||||
.rail-pill-muted {
|
||||
color: #4d3a29;
|
||||
background: linear-gradient(180deg, #f7f2e8, #e0d6c5);
|
||||
}
|
||||
|
||||
.rail-hold-progress {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 240, 199, 0.32);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 237, 208, 0.12);
|
||||
}
|
||||
|
||||
.rail-hold-progress-bar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: left center;
|
||||
background: linear-gradient(90deg, #ffe9a8, #fff7d0, #ffffff);
|
||||
box-shadow: 0 0 12px rgba(255, 246, 203, 0.5);
|
||||
}
|
||||
|
||||
.voice-settings-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1924,6 +1962,14 @@
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.rail-pill-hold-wrap {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rail-hold-progress {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.finish-dialog {
|
||||
padding: 20px 14px 14px;
|
||||
border-radius: 18px;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user