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

511 lines
11 KiB
Go

package app
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"daily-timer/internal/models"
"daily-timer/internal/services/updater"
"daily-timer/internal/storage"
"daily-timer/internal/timer"
"daily-timer/internal/version"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type App struct {
ctx context.Context
store *storage.Storage
timer *timer.Timer
session *models.MeetingSession
currentLogs map[uint]*models.ParticipantLog
updater *updater.Updater
}
func New(store *storage.Storage) *App {
return &App{
store: store,
currentLogs: make(map[uint]*models.ParticipantLog),
updater: updater.New(),
}
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) OnDomReady(ctx context.Context) {
runtime.WindowShow(ctx)
}
func (a *App) Shutdown(ctx context.Context) {
if a.timer != nil {
a.timer.Close()
}
if a.store != nil {
_ = a.store.Close()
}
}
// Participants
func (a *App) GetParticipants() ([]models.Participant, error) {
return a.store.GetParticipants()
}
func (a *App) AddParticipant(name string, email string, timeLimit int) (*models.Participant, error) {
participants, _ := a.store.GetAllParticipants()
order := len(participants)
p := &models.Participant{
Name: name,
Email: email,
TimeLimit: timeLimit,
Order: order,
Active: true,
}
if err := a.store.CreateParticipant(p); err != nil {
return nil, err
}
return p, nil
}
func (a *App) UpdateParticipant(id uint, name string, email string, timeLimit int) error {
p := &models.Participant{
ID: id,
Name: name,
Email: email,
TimeLimit: timeLimit,
}
return a.store.UpdateParticipant(p)
}
func (a *App) DeleteParticipant(id uint) error {
return a.store.DeleteParticipant(id)
}
func (a *App) ReorderParticipants(ids []uint) error {
return a.store.ReorderParticipants(ids)
}
// Meeting
func (a *App) GetMeeting() (*models.Meeting, error) {
return a.store.GetMeeting()
}
func (a *App) UpdateMeeting(name string, timeLimit int) error {
meeting, err := a.store.GetMeeting()
if err != nil {
return err
}
meeting.Name = name
meeting.TimeLimit = timeLimit
return a.store.UpdateMeeting(meeting)
}
// Timer Controls
func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) error {
meeting, err := a.store.GetMeeting()
if err != nil {
return err
}
settings, _ := a.store.GetSettings()
warningThreshold := 30
if settings != nil {
warningThreshold = settings.WarningThreshold
}
session, err := a.store.CreateSession(meeting.ID)
if err != nil {
return err
}
a.session = session
for participantID, present := range attendance {
_ = a.store.SetAttendance(session.ID, participantID, present, false)
}
participants, _ := a.store.GetParticipants()
participantMap := make(map[uint]models.Participant)
for _, p := range participants {
participantMap[p.ID] = p
}
queue := make([]models.QueuedSpeaker, 0, len(participantOrder))
for i, id := range participantOrder {
if p, ok := participantMap[id]; ok {
if present, ok := attendance[id]; ok && present {
queue = append(queue, models.QueuedSpeaker{
ID: p.ID,
Name: p.Name,
TimeLimit: p.TimeLimit,
Order: i,
})
}
}
}
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
a.timer.SetQueue(queue)
a.currentLogs = make(map[uint]*models.ParticipantLog)
go a.handleTimerEvents()
a.timer.Start()
return nil
}
func (a *App) handleTimerEvents() {
for event := range a.timer.Events() {
switch event.Type {
case timer.EventTick:
runtime.EventsEmit(a.ctx, "timer:tick", event.State)
case timer.EventSpeakerWarning:
runtime.EventsEmit(a.ctx, "timer:speaker_warning", event.State)
case timer.EventSpeakerTimeUp:
runtime.EventsEmit(a.ctx, "timer:speaker_timeup", event.State)
case timer.EventMeetingWarning:
runtime.EventsEmit(a.ctx, "timer:meeting_warning", event.State)
case timer.EventMeetingTimeUp:
runtime.EventsEmit(a.ctx, "timer:meeting_timeup", event.State)
case timer.EventSpeakerChanged:
a.saveSpeakerLog(event.State)
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
case timer.EventMeetingEnded:
a.saveSpeakerLog(event.State)
a.endMeetingSession(event.State)
runtime.EventsEmit(a.ctx, "timer:meeting_ended", event.State)
}
}
}
func (a *App) saveSpeakerLog(state models.TimerState) {
if a.session == nil {
return
}
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)
}
}
if state.CurrentSpeakerID > 0 {
log := &models.ParticipantLog{
SessionID: a.session.ID,
ParticipantID: state.CurrentSpeakerID,
StartedAt: time.Now(),
Order: state.SpeakingOrder,
}
_ = a.store.CreateParticipantLog(log)
a.currentLogs[state.CurrentSpeakerID] = log
}
}
func (a *App) endMeetingSession(state models.TimerState) {
if a.session == nil {
return
}
_ = a.store.EndSession(a.session.ID, state.MeetingElapsed)
a.session = nil
a.currentLogs = make(map[uint]*models.ParticipantLog)
}
func (a *App) NextSpeaker() {
if a.timer != nil {
a.timer.NextSpeaker()
}
}
func (a *App) SkipSpeaker() {
if a.timer != nil {
a.timer.SkipSpeaker()
}
}
func (a *App) RemoveFromQueue(speakerID uint) {
if a.timer != nil {
a.timer.RemoveFromQueue(speakerID)
}
}
func (a *App) PauseMeeting() {
if a.timer != nil {
a.timer.Pause()
}
}
func (a *App) ResumeMeeting() {
if a.timer != nil {
a.timer.Resume()
}
}
func (a *App) StopMeeting() {
if a.timer != nil {
a.timer.Stop()
}
}
func (a *App) GetTimerState() *models.TimerState {
if a.timer == nil {
return nil
}
state := a.timer.GetState()
return &state
}
// Settings
func (a *App) GetSettings() (*models.Settings, error) {
return a.store.GetSettings()
}
func (a *App) UpdateSettings(settings *models.Settings) error {
return a.store.UpdateSettings(settings)
}
// History & Statistics
func (a *App) GetSessions(limit, offset int) ([]models.MeetingSession, error) {
return a.store.GetSessions(limit, offset)
}
func (a *App) GetSession(id uint) (*models.MeetingSession, error) {
return a.store.GetSession(id)
}
func (a *App) DeleteSession(id uint) error {
return a.store.DeleteSession(id)
}
func (a *App) DeleteAllSessions() error {
return a.store.DeleteAllSessions()
}
func (a *App) GetStatistics(fromStr, toStr string) (*models.AggregatedStats, error) {
from, err := time.Parse("2006-01-02", fromStr)
if err != nil {
from = time.Now().AddDate(0, -1, 0)
}
to, err := time.Parse("2006-01-02", toStr)
if err != nil {
to = time.Now()
}
return a.store.GetAggregatedStats(from, to)
}
func (a *App) ExportData(fromStr, toStr string) (string, error) {
from, _ := time.Parse("2006-01-02", fromStr)
to, _ := time.Parse("2006-01-02", toStr)
if from.IsZero() {
from = time.Now().AddDate(-1, 0, 0)
}
if to.IsZero() {
to = time.Now()
}
participants, err := a.store.GetAllParticipants()
if err != nil {
return "", err
}
sessions, err := a.store.GetSessions(1000, 0)
if err != nil {
return "", err
}
meeting, _ := a.store.GetMeeting()
sessionStats := make([]models.SessionStats, 0, len(sessions))
for _, s := range sessions {
if s.StartedAt.Before(from) || s.StartedAt.After(to) {
continue
}
stats := models.SessionStats{
SessionID: s.ID,
Date: s.StartedAt.Format("2006-01-02 15:04"),
TotalDuration: s.TotalDuration,
MeetingLimit: meeting.TimeLimit,
Overtime: s.TotalDuration > meeting.TimeLimit,
}
for _, log := range s.ParticipantLogs {
stats.ParticipantStats = append(stats.ParticipantStats, models.ParticipantStats{
ParticipantID: log.ParticipantID,
Name: log.Participant.Name,
Duration: log.Duration,
TimeLimit: log.Participant.TimeLimit,
Overtime: log.Overtime,
Skipped: log.Skipped,
SpeakingOrder: log.Order,
})
if log.Overtime {
stats.OvertimeCount++
}
if log.Skipped {
stats.SkippedCount++
}
}
for _, att := range s.Attendance {
if att.Present {
stats.PresentCount++
} else {
stats.AbsentCount++
}
}
stats.ParticipantCount = stats.PresentCount + stats.AbsentCount
sessionStats = append(sessionStats, stats)
}
aggStats, _ := a.store.GetAggregatedStats(from, to)
exportData := models.ExportData{
ExportedAt: time.Now().Format(time.RFC3339),
Participants: participants,
Sessions: sessionStats,
Statistics: *aggStats,
}
data, err := json.MarshalIndent(exportData, "", " ")
if err != nil {
return "", err
}
filename := fmt.Sprintf("daily-timer-export-%s.json", time.Now().Format("2006-01-02"))
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: filename,
Filters: []runtime.FileFilter{
{DisplayName: "JSON Files", Pattern: "*.json"},
},
})
if err != nil {
return "", err
}
if savePath == "" {
return "", nil
}
if err := os.WriteFile(savePath, data, 0644); err != nil {
return "", err
}
return savePath, nil
}
func (a *App) ExportCSV(fromStr, toStr string) (string, error) {
from, _ := time.Parse("2006-01-02", fromStr)
to, _ := time.Parse("2006-01-02", toStr)
if from.IsZero() {
from = time.Now().AddDate(-1, 0, 0)
}
if to.IsZero() {
to = time.Now()
}
sessions, err := a.store.GetSessions(1000, 0)
if err != nil {
return "", err
}
csv := "Date,Participant,Duration (s),Time Limit (s),Overtime,Skipped,Speaking Order\n"
for _, s := range sessions {
if s.StartedAt.Before(from) || s.StartedAt.After(to) {
continue
}
date := s.StartedAt.Format("2006-01-02")
for _, log := range s.ParticipantLogs {
csv += fmt.Sprintf("%s,%s,%d,%d,%t,%t,%d\n",
date,
log.Participant.Name,
log.Duration,
log.Participant.TimeLimit,
log.Overtime,
log.Skipped,
log.Order,
)
}
}
filename := fmt.Sprintf("daily-timer-export-%s.csv", time.Now().Format("2006-01-02"))
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: filename,
Filters: []runtime.FileFilter{
{DisplayName: "CSV Files", Pattern: "*.csv"},
},
})
if err != nil {
return "", err
}
if savePath == "" {
return "", nil
}
if err := os.WriteFile(savePath, []byte(csv), 0644); err != nil {
return "", err
}
return savePath, nil
}
// Sound
func (a *App) GetSoundsDir() string {
configDir, _ := os.UserConfigDir()
soundsDir := filepath.Join(configDir, "DailyTimer", "sounds")
_ = os.MkdirAll(soundsDir, 0755)
return soundsDir
}
// Updates
func (a *App) GetVersion() string {
return version.Version
}
func (a *App) CheckForUpdates() (*updater.UpdateInfo, error) {
return a.updater.CheckForUpdates()
}
func (a *App) DownloadAndInstallUpdate() error {
err := a.updater.DownloadAndInstall(func(progress float64) {
runtime.EventsEmit(a.ctx, "update:progress", progress)
})
if err != nil {
return err
}
runtime.EventsEmit(a.ctx, "update:complete", true)
return nil
}
func (a *App) RestartApp() error {
return a.updater.RestartApp()
}