fix: layout, hotkeys, skip/switch speaker logic

This commit is contained in:
Mikhail Kiselev
2026-02-10 23:10:02 +03:00
parent fc93ebbd26
commit fe6a41226c
8 changed files with 310 additions and 26 deletions

View File

@@ -179,13 +179,37 @@ func (a *App) handleTimerEvents() {
a.saveSpeakerLog(event.State)
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
case timer.EventMeetingEnded:
a.saveSpeakerLog(event.State)
a.finalizeSpeakerLogs(event.State)
a.endMeetingSession(event.State)
runtime.EventsEmit(a.ctx, "timer:meeting_ended", event.State)
}
}
}
func (a *App) finalizeSpeakerLogs(state models.TimerState) {
if a.session == nil {
return
}
// Only finalize existing logs, don't create new ones
for id, log := range a.currentLogs {
if log.EndedAt == nil {
now := time.Now()
log.EndedAt = &now
log.Duration = int(now.Sub(log.StartedAt).Seconds())
participants, _ := a.store.GetParticipants()
for _, p := range participants {
if p.ID == id {
log.Overtime = log.Duration > p.TimeLimit
break
}
}
_ = a.store.UpdateParticipantLog(log)
}
}
}
func (a *App) saveSpeakerLog(state models.TimerState) {
if a.session == nil {
return
@@ -247,6 +271,12 @@ func (a *App) RemoveFromQueue(speakerID uint) {
}
}
func (a *App) SwitchToSpeaker(speakerID uint) {
if a.timer != nil {
a.timer.SwitchToSpeaker(speakerID)
}
}
func (a *App) PauseMeeting() {
if a.timer != nil {
a.timer.Pause()

View File

@@ -106,14 +106,14 @@ func (t *Timer) Start() {
t.meetingWarned = false
if len(t.queue) > 0 {
t.startNextSpeaker(now)
t.startNextSpeaker(now, 0)
}
t.mu.Unlock()
go t.tick()
}
func (t *Timer) startNextSpeaker(now time.Time) {
func (t *Timer) startNextSpeaker(now time.Time, offset time.Duration) {
if len(t.queue) == 0 {
return
}
@@ -137,8 +137,8 @@ func (t *Timer) startNextSpeaker(now time.Time) {
t.currentSpeakerID = speaker.ID
t.currentSpeaker = speaker.Name
t.speakerLimit = time.Duration(speaker.TimeLimit) * time.Second
t.speakerStartTime = now
t.speakerElapsed = 0
t.speakerStartTime = now.Add(-offset)
t.speakerElapsed = offset
t.speakingOrder++
t.speakerWarned = false
t.speakerTimeUpEmitted = false
@@ -193,7 +193,7 @@ func (t *Timer) NextSpeaker() {
var eventType EventType
if len(t.queue) > 0 {
t.startNextSpeaker(now)
t.startNextSpeaker(now, 0)
eventType = EventSpeakerChanged
} else {
t.running = false
@@ -227,11 +227,14 @@ func (t *Timer) SkipSpeaker() {
now := time.Now()
if len(t.queue) > 1 {
t.startNextSpeaker(now)
t.startNextSpeaker(now, 0)
t.mu.Unlock()
t.emit(EventSpeakerChanged)
} else {
// Only skipped speaker left - they need to speak now
t.startNextSpeaker(now, 0)
t.mu.Unlock()
t.emit(EventSpeakerChanged)
}
}
@@ -248,7 +251,16 @@ func (t *Timer) RemoveFromQueue(speakerID uint) {
return
}
// Remove from queue
// Find speaker info before removing
var speakerInfo models.QueuedSpeaker
for _, s := range t.queue {
if s.ID == speakerID {
speakerInfo = s
break
}
}
// Remove from current position in queue
for i, s := range t.queue {
if s.ID == speakerID {
t.queue = append(t.queue[:i], t.queue[i+1:]...)
@@ -256,11 +268,124 @@ func (t *Timer) RemoveFromQueue(speakerID uint) {
}
}
// Add to end of queue so they can speak later
if speakerInfo.ID != 0 {
t.queue = append(t.queue, speakerInfo)
}
// Mark as skipped in allSpeakers and move to end
t.updateSpeakerStatus(speakerID, models.SpeakerStatusSkipped)
t.moveSpeakerToEnd(speakerID)
}
// SwitchToSpeaker moves the specified speaker to front of queue and starts them
// If speaker is already done, resumes their timer from accumulated time
func (t *Timer) SwitchToSpeaker(speakerID uint) {
t.mu.Lock()
if !t.running {
t.mu.Unlock()
return
}
// First, find speaker in allSpeakers to get their info and status
var speakerInfo *models.SpeakerInfo
var speakerInfoIdx int
for i := range t.allSpeakers {
if t.allSpeakers[i].ID == speakerID {
speakerInfo = &t.allSpeakers[i]
speakerInfoIdx = i
break
}
}
if speakerInfo == nil {
t.mu.Unlock()
return
}
// Don't switch to currently speaking speaker
if speakerInfo.Status == models.SpeakerStatusSpeaking {
t.mu.Unlock()
return
}
// Calculate offset for resuming (0 for pending/skipped, timeSpent for done)
var offset time.Duration
if speakerInfo.Status == models.SpeakerStatusDone {
offset = time.Duration(speakerInfo.TimeSpent) * time.Second
}
// Save current speaker time
now := time.Now()
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
}
}
}
// Find speaker in queue (pending/skipped) or create new entry (done)
foundIdx := -1
for i, s := range t.queue {
if s.ID == speakerID {
foundIdx = i
break
}
}
// Create QueuedSpeaker from SpeakerInfo
queuedSpeaker := models.QueuedSpeaker{
ID: speakerInfo.ID,
Name: speakerInfo.Name,
TimeLimit: speakerInfo.TimeLimit,
Order: speakerInfo.Order,
}
if foundIdx >= 0 {
// Remove from current position in queue
t.queue = append(t.queue[:foundIdx], t.queue[foundIdx+1:]...)
}
// Insert at front of queue
t.queue = append([]models.QueuedSpeaker{queuedSpeaker}, t.queue...)
// Move the selected speaker in allSpeakers to position after last done/speaking
insertPos := 0
for i, s := range t.allSpeakers {
if s.Status == models.SpeakerStatusDone || s.Status == models.SpeakerStatusSpeaking {
insertPos = i + 1
}
}
if speakerInfoIdx >= 0 && speakerInfoIdx != insertPos {
// Save speaker info before removing
savedInfo := *speakerInfo
// Remove from current position
t.allSpeakers = append(t.allSpeakers[:speakerInfoIdx], t.allSpeakers[speakerInfoIdx+1:]...)
// Adjust insert position if needed
if speakerInfoIdx < insertPos {
insertPos--
}
// Insert at new position
t.allSpeakers = append(t.allSpeakers[:insertPos], append([]models.SpeakerInfo{savedInfo}, t.allSpeakers[insertPos:]...)...)
// Update order numbers
for i := range t.allSpeakers {
t.allSpeakers[i].Order = i + 1
}
}
// Start this speaker with offset (0 for new speakers, accumulated time for done)
t.startNextSpeaker(now, offset)
t.mu.Unlock()
t.emit(EventSpeakerChanged)
}
func (t *Timer) Pause() {
t.mu.Lock()