feat: show spent time in participant list, fix timer sounds

This commit is contained in:
Mikhail Kiselev
2026-02-10 18:15:18 +03:00
parent 850d1deed2
commit f0a8c32ea2
4 changed files with 70 additions and 12 deletions

View File

@@ -162,7 +162,13 @@
}
}
function playSound(name) {
async function playSound(name) {
// Ensure AudioContext is running (may be suspended after inactivity)
const ctx = getAudioContext()
if (ctx.state === 'suspended') {
await ctx.resume()
}
switch (name) {
case 'warning':
playBeep(880, 0.15)

View File

@@ -8,10 +8,17 @@
$: allSpeakers = timerState?.allSpeakers || []
$: currentSpeakerId = timerState?.currentSpeakerId || 0
$: currentElapsed = timerState?.speakerElapsed || 0
function handleSkip(speakerId) {
dispatch('skip', { speakerId })
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
</script>
<div class="participant-list">
@@ -23,7 +30,23 @@
<li class="speaker-item {speaker.status}">
<span class="order">{speaker.order}</span>
<span class="name">{speaker.name}</span>
<span class="time-limit">{Math.floor(speaker.timeLimit / 60)}:{(speaker.timeLimit % 60).toString().padStart(2, '0')}</span>
<span class="time-display">
{#if speaker.status === 'done'}
<span class="time-spent" class:overtime={speaker.timeSpent > speaker.timeLimit}>
{formatTime(speaker.timeSpent)}
</span>
<span class="time-sep">/</span>
<span class="time-limit">{formatTime(speaker.timeLimit)}</span>
{:else if speaker.status === 'speaking'}
<span class="time-spent" class:overtime={currentElapsed > speaker.timeLimit}>
{formatTime(currentElapsed)}
</span>
<span class="time-sep">/</span>
<span class="time-limit">{formatTime(speaker.timeLimit)}</span>
{:else}
<span class="time-limit">{formatTime(speaker.timeLimit)}</span>
{/if}
</span>
{#if speaker.status === 'pending' || speaker.status === 'skipped'}
<button class="skip-btn" on:click={() => handleSkip(speaker.id)} title="{$t('controls.skip')}">
@@ -129,13 +152,6 @@
white-space: nowrap;
}
.time-limit {
font-family: 'SF Mono', 'Menlo', monospace;
font-size: 12px;
color: #6b7280;
flex-shrink: 0;
}
.skip-btn {
margin-left: 8px;
padding: 4px 8px;
@@ -163,4 +179,29 @@
text-align: center;
padding: 24px;
}
.time-display {
display: flex;
align-items: center;
gap: 2px;
font-family: 'SF Mono', 'Menlo', monospace;
font-size: 12px;
flex-shrink: 0;
}
.time-spent {
color: #4ade80;
}
.time-spent.overtime {
color: #ef4444;
}
.time-sep {
color: #6b7280;
}
.time-limit {
color: #6b7280;
}
</style>

View File

@@ -33,6 +33,7 @@ type SpeakerInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
TimeLimit int `json:"timeLimit"`
TimeSpent int `json:"timeSpent"`
Order int `json:"order"`
Status SpeakerStatus `json:"status"`
}

View File

@@ -118,12 +118,14 @@ func (t *Timer) startNextSpeaker(now time.Time) {
return
}
// Mark previous speaker as done (only if they were speaking, not skipped)
// Mark previous speaker as done and save their time spent
if t.currentSpeakerID != 0 {
timeSpent := int(now.Sub(t.speakerStartTime).Seconds())
for i := range t.allSpeakers {
if t.allSpeakers[i].ID == t.currentSpeakerID {
if t.allSpeakers[i].Status == models.SpeakerStatusSpeaking {
t.allSpeakers[i].Status = models.SpeakerStatusDone
t.allSpeakers[i].TimeSpent = timeSpent
}
break
}
@@ -299,9 +301,17 @@ func (t *Timer) Stop() {
t.mu.Unlock()
return
}
// Mark current speaker as done before stopping
// Mark current speaker as done and save their time spent
if t.currentSpeakerID != 0 {
t.updateSpeakerStatus(t.currentSpeakerID, models.SpeakerStatusDone)
now := time.Now()
timeSpent := int(now.Sub(t.speakerStartTime).Seconds())
for i := range t.allSpeakers {
if t.allSpeakers[i].ID == t.currentSpeakerID {
t.allSpeakers[i].Status = models.SpeakerStatusDone
t.allSpeakers[i].TimeSpent = timeSpent
break
}
}
}
t.running = false
t.paused = false