Files
daily-timer/internal/timer/timer.go
2026-02-10 18:15:18 +03:00

439 lines
9.5 KiB
Go

package timer
import (
"context"
"sync"
"time"
"daily-timer/internal/models"
)
type EventType string
const (
EventTick EventType = "tick"
EventSpeakerWarning EventType = "speaker_warning"
EventSpeakerTimeUp EventType = "speaker_timeup"
EventMeetingWarning EventType = "meeting_warning"
EventMeetingTimeUp EventType = "meeting_timeup"
EventSpeakerChanged EventType = "speaker_changed"
EventMeetingEnded EventType = "meeting_ended"
)
type Event struct {
Type EventType `json:"type"`
State models.TimerState `json:"state"`
}
type Timer struct {
mu sync.RWMutex
running bool
paused bool
meetingStartTime time.Time
meetingElapsed time.Duration
meetingLimit time.Duration
speakerStartTime time.Time
speakerElapsed time.Duration
speakerLimit time.Duration
currentSpeakerID uint
currentSpeaker string
speakingOrder int
queue []models.QueuedSpeaker
allSpeakers []models.SpeakerInfo
warningThreshold time.Duration
speakerWarned bool
speakerTimeUpEmitted bool
meetingWarned bool
eventCh chan Event
ctx context.Context
cancel context.CancelFunc
pausedAt time.Time
}
func New(meetingLimitSec, warningThresholdSec int) *Timer {
ctx, cancel := context.WithCancel(context.Background())
return &Timer{
meetingLimit: time.Duration(meetingLimitSec) * time.Second,
warningThreshold: time.Duration(warningThresholdSec) * time.Second,
eventCh: make(chan Event, 100),
ctx: ctx,
cancel: cancel,
}
}
func (t *Timer) Events() <-chan Event {
return t.eventCh
}
func (t *Timer) SetQueue(speakers []models.QueuedSpeaker) {
t.mu.Lock()
defer t.mu.Unlock()
t.queue = speakers
// Initialize allSpeakers with pending status
t.allSpeakers = make([]models.SpeakerInfo, len(speakers))
for i, s := range speakers {
t.allSpeakers[i] = models.SpeakerInfo{
ID: s.ID,
Name: s.Name,
TimeLimit: s.TimeLimit,
Order: i + 1,
Status: models.SpeakerStatusPending,
}
}
}
func (t *Timer) Start() {
t.mu.Lock()
if t.running {
t.mu.Unlock()
return
}
now := time.Now()
t.running = true
t.paused = false
t.meetingStartTime = now
t.meetingElapsed = 0
t.speakingOrder = 0
t.speakerWarned = false
t.meetingWarned = false
if len(t.queue) > 0 {
t.startNextSpeaker(now)
}
t.mu.Unlock()
go t.tick()
}
func (t *Timer) startNextSpeaker(now time.Time) {
if len(t.queue) == 0 {
return
}
// 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
}
}
}
speaker := t.queue[0]
t.queue = t.queue[1:]
t.currentSpeakerID = speaker.ID
t.currentSpeaker = speaker.Name
t.speakerLimit = time.Duration(speaker.TimeLimit) * time.Second
t.speakerStartTime = now
t.speakerElapsed = 0
t.speakingOrder++
t.speakerWarned = false
t.speakerTimeUpEmitted = false
// Mark current speaker as speaking
t.updateSpeakerStatus(speaker.ID, models.SpeakerStatusSpeaking)
}
func (t *Timer) updateSpeakerStatus(id uint, status models.SpeakerStatus) {
for i := range t.allSpeakers {
if t.allSpeakers[i].ID == id {
t.allSpeakers[i].Status = status
break
}
}
}
func (t *Timer) moveSpeakerToEnd(id uint) {
var speaker models.SpeakerInfo
idx := -1
for i := range t.allSpeakers {
if t.allSpeakers[i].ID == id {
speaker = t.allSpeakers[i]
idx = i
break
}
}
if idx >= 0 {
// Remove from current position
t.allSpeakers = append(t.allSpeakers[:idx], t.allSpeakers[idx+1:]...)
// Add to end
t.allSpeakers = append(t.allSpeakers, speaker)
// Update order numbers
for i := range t.allSpeakers {
t.allSpeakers[i].Order = i + 1
}
}
}
func (t *Timer) NextSpeaker() {
t.mu.Lock()
if !t.running {
t.mu.Unlock()
return
}
now := time.Now()
if !t.paused {
t.speakerElapsed = now.Sub(t.speakerStartTime)
}
var eventType EventType
if len(t.queue) > 0 {
t.startNextSpeaker(now)
eventType = EventSpeakerChanged
} else {
t.running = false
t.paused = false
eventType = EventMeetingEnded
}
t.mu.Unlock()
t.emit(eventType)
}
func (t *Timer) SkipSpeaker() {
t.mu.Lock()
if !t.running || t.currentSpeakerID == 0 {
t.mu.Unlock()
return
}
// Mark current speaker as skipped in allSpeakers
t.updateSpeakerStatus(t.currentSpeakerID, models.SpeakerStatusSkipped)
skipped := models.QueuedSpeaker{
ID: t.currentSpeakerID,
Name: t.currentSpeaker,
TimeLimit: int(t.speakerLimit.Seconds()),
}
t.queue = append(t.queue, skipped)
// Move skipped speaker to end of allSpeakers list
t.moveSpeakerToEnd(t.currentSpeakerID)
now := time.Now()
if len(t.queue) > 1 {
t.startNextSpeaker(now)
t.mu.Unlock()
t.emit(EventSpeakerChanged)
} else {
t.mu.Unlock()
}
}
func (t *Timer) RemoveFromQueue(speakerID uint) {
t.mu.Lock()
defer t.mu.Unlock()
if !t.running {
return
}
// Don't remove current speaker - use SkipSpeaker for that
if speakerID == t.currentSpeakerID {
return
}
// Remove from queue
for i, s := range t.queue {
if s.ID == speakerID {
t.queue = append(t.queue[:i], t.queue[i+1:]...)
break
}
}
// Mark as skipped in allSpeakers and move to end
t.updateSpeakerStatus(speakerID, models.SpeakerStatusSkipped)
t.moveSpeakerToEnd(speakerID)
}
func (t *Timer) Pause() {
t.mu.Lock()
if !t.running || t.paused {
t.mu.Unlock()
return
}
now := time.Now()
t.paused = true
t.pausedAt = now
t.speakerElapsed = now.Sub(t.speakerStartTime)
t.meetingElapsed = now.Sub(t.meetingStartTime)
t.mu.Unlock()
t.emit(EventTick)
}
func (t *Timer) Resume() {
t.mu.Lock()
if !t.running || !t.paused {
t.mu.Unlock()
return
}
pauseDuration := time.Since(t.pausedAt)
t.speakerStartTime = t.speakerStartTime.Add(pauseDuration)
t.meetingStartTime = t.meetingStartTime.Add(pauseDuration)
t.paused = false
t.mu.Unlock()
t.emit(EventTick)
}
func (t *Timer) Stop() {
t.mu.Lock()
if !t.running {
t.mu.Unlock()
return
}
// Mark current speaker as done and save their time spent
if t.currentSpeakerID != 0 {
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
t.mu.Unlock()
t.emit(EventMeetingEnded)
}
func (t *Timer) GetState() models.TimerState {
t.mu.RLock()
defer t.mu.RUnlock()
return t.buildState()
}
func (t *Timer) buildState() models.TimerState {
speakerElapsed := t.speakerElapsed
meetingElapsed := t.meetingElapsed
if t.running && !t.paused {
now := time.Now()
speakerElapsed = now.Sub(t.speakerStartTime)
meetingElapsed = now.Sub(t.meetingStartTime)
}
speakerOvertime := speakerElapsed > t.speakerLimit
meetingOvertime := meetingElapsed > t.meetingLimit
warning := !speakerOvertime && (t.speakerLimit-speakerElapsed) <= t.warningThreshold
// Copy allSpeakers to avoid data race and calculate total speakers time
allSpeakers := make([]models.SpeakerInfo, len(t.allSpeakers))
copy(allSpeakers, t.allSpeakers)
totalSpeakersTime := 0
for _, s := range t.allSpeakers {
totalSpeakersTime += s.TimeLimit
}
return models.TimerState{
Running: t.running,
Paused: t.paused,
CurrentSpeakerID: t.currentSpeakerID,
CurrentSpeaker: t.currentSpeaker,
SpeakerElapsed: int(speakerElapsed.Seconds()),
SpeakerLimit: int(t.speakerLimit.Seconds()),
MeetingElapsed: int(meetingElapsed.Seconds()),
MeetingLimit: int(t.meetingLimit.Seconds()),
SpeakerOvertime: speakerOvertime,
MeetingOvertime: meetingOvertime,
Warning: warning,
WarningSeconds: int(t.warningThreshold.Seconds()),
TotalSpeakersTime: totalSpeakersTime,
SpeakingOrder: t.speakingOrder,
TotalSpeakers: len(t.allSpeakers),
RemainingQueue: t.queue,
AllSpeakers: allSpeakers,
}
}
func (t *Timer) tick() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-t.ctx.Done():
return
case <-ticker.C:
t.mu.Lock()
if !t.running {
t.mu.Unlock()
return
}
if t.paused {
t.mu.Unlock()
continue
}
now := time.Now()
speakerElapsed := now.Sub(t.speakerStartTime)
meetingElapsed := now.Sub(t.meetingStartTime)
remaining := t.speakerLimit - speakerElapsed
if !t.speakerWarned && remaining <= t.warningThreshold && remaining > 0 {
t.speakerWarned = true
t.mu.Unlock()
t.emit(EventSpeakerWarning)
continue
}
if !t.speakerTimeUpEmitted && speakerElapsed >= t.speakerLimit {
t.speakerTimeUpEmitted = true
t.mu.Unlock()
t.emit(EventSpeakerTimeUp)
continue
}
meetingRemaining := t.meetingLimit - meetingElapsed
if !t.meetingWarned && meetingRemaining <= t.warningThreshold && meetingRemaining > 0 {
t.meetingWarned = true
t.mu.Unlock()
t.emit(EventMeetingWarning)
continue
}
t.mu.Unlock()
t.emit(EventTick)
}
}
}
func (t *Timer) emit(eventType EventType) {
t.mu.RLock()
state := t.buildState()
t.mu.RUnlock()
select {
case t.eventCh <- Event{Type: eventType, State: state}:
default:
}
}
func (t *Timer) Close() {
t.cancel()
close(t.eventCh)
}