diff --git a/index.html b/index.html index 58fa15c..248ca61 100644 --- a/index.html +++ b/index.html @@ -23,12 +23,14 @@

Pomodoro Timer

Connecting to server...
- + - + + diff --git a/internal/broadcast/broadcast.go b/internal/broadcast/broadcast.go index deb3010..229ae67 100644 --- a/internal/broadcast/broadcast.go +++ b/internal/broadcast/broadcast.go @@ -8,7 +8,7 @@ import ( ) // BroadcastMessage sends a message to all connected WebSocket clients. -func BroadcastMessage(clients map[*websocket.Conn]bool, message models.BroadcastMessage) { +func BroadcastMessage(clients map[*websocket.Conn]*models.Client, message models.BroadcastMessage) { // Marshal the message into JSON format jsonMessage, err := json.Marshal(message) if err != nil { @@ -17,12 +17,11 @@ func BroadcastMessage(clients map[*websocket.Conn]bool, message models.Broadcast } // Iterate over all connected clients and broadcast the message - for client := range clients { - err := client.WriteMessage(websocket.TextMessage, jsonMessage) + for _, client := range clients { + err := client.SendMessage(websocket.TextMessage, jsonMessage) if err != nil { log.Printf("Error broadcasting to client: %v", err) - client.Close() - delete(clients, client) // Remove the client if an error occurs + // The client is responsible for closing itself on error } } } diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 65705fe..c8a3b03 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -1,6 +1,8 @@ package pomodoro import ( + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "sync" ) @@ -13,12 +15,20 @@ const ( ) var pomodoroRunning bool +var pomodoroPaused bool + +var pomodoroResetChannel = make(chan bool, 1) +var pomodoroPauseChannel = make(chan bool, 1) +var pomodoroResumeChannel = make(chan bool, 1) + var mu sync.Mutex // to synchronize access to shared state // RunPomodoroTimer iterates the Pomodoro work/break sessions. -func RunPomodoroTimer(clients map[*websocket.Conn]bool) { +func RunPomodoroTimer(clients map[*websocket.Conn]*models.Client) { mu.Lock() pomodoroRunning = true + pomodoroPaused = false + mu.Unlock() for session := 1; session <= sessions; session++ { if !startTimer(clients, workDuration, "Work", session) { @@ -35,11 +45,53 @@ func RunPomodoroTimer(clients map[*websocket.Conn]bool) { } } + mu.Lock() pomodoroRunning = false mu.Unlock() } -// IsPomodoroRunning returns the status of the timer. +// ResetPomodoro resets the running Pomodoro timer. +func ResetPomodoro(clients map[*websocket.Conn]*models.Client) { + mu.Lock() + pomodoroRunning = false // Reset the running state + pomodoroPaused = false // Reset the paused state + mu.Unlock() + + // Broadcast the reset message to all clients + broadcast.BroadcastMessage(clients, models.BroadcastMessage{ + Mode: "none", + Session: 0, + MaxSession: 0, + TimeLeft: 0, + }) + + // Send a reset signal to stop any running timers + pomodoroResetChannel <- true +} + +func PausePomodoro() { + mu.Lock() + pomodoroPaused = true + mu.Unlock() + + pomodoroPauseChannel <- true +} + +func ResumePomodoro() { + mu.Lock() + pomodoroPaused = false + mu.Unlock() + pomodoroResumeChannel <- true +} + func IsPomodoroRunning() bool { + mu.Lock() + defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done return pomodoroRunning } + +func IsPomodoroPaused() bool { + mu.Lock() + defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done + return pomodoroPaused +} diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index c7d931b..96595b3 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -7,24 +7,28 @@ import ( "time" ) -var timerStopChannel = make(chan bool, 1) - // startTimer runs the countdown and broadcasts every second. -func startTimer(clients map[*websocket.Conn]bool, remainingSeconds int, mode string, session int) bool { +func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string, session int) bool { for remainingSeconds > 0 { select { - case <-timerStopChannel: - return false // Stop the timer if a stop command is received + case <-pomodoroResetChannel: + return false + case <-pomodoroPauseChannel: + // Nothing to set here, just waiting for the signal + case <-pomodoroResumeChannel: + // Nothing to set here, just waiting for the signal default: // Broadcast the current state to all clients - broadcast.BroadcastMessage(clients, models.BroadcastMessage{ - Mode: mode, - Session: session, - MaxSession: sessions, - TimeLeft: remainingSeconds, - }) - time.Sleep(time.Second) - remainingSeconds-- + if !IsPomodoroPaused() { + broadcast.BroadcastMessage(clients, models.BroadcastMessage{ + Mode: mode, + Session: session, + MaxSession: sessions, + TimeLeft: remainingSeconds, + }) + time.Sleep(time.Second) + remainingSeconds-- + } } } @@ -38,8 +42,3 @@ func startTimer(clients map[*websocket.Conn]bool, remainingSeconds int, mode str return true } - -// StopTimer sends a signal to stop the running Pomodoro timer. -func StopTimer() { - timerStopChannel <- true -} diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index 41855bf..91ee750 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -6,10 +6,22 @@ import ( "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" + "sync" ) +// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct +var Clients = make(map[*websocket.Conn]*models.Client) +var mu sync.Mutex // Mutex to protect access to the Clients map + // handleClientCommands listens for commands from WebSocket clients and dispatches to the timer. func handleClientCommands(ws *websocket.Conn) { + // Create a new Client and add it to the Clients map + mu.Lock() + Clients[ws] = &models.Client{ + Conn: ws, + } + mu.Unlock() + for { _, message, err := ws.ReadMessage() if err != nil { @@ -34,8 +46,17 @@ func handleClientCommands(ws *websocket.Conn) { } case "stop": if pomodoro.IsPomodoroRunning() { - pomodoro.StopTimer() // Stop the timer in the Pomodoro package + pomodoro.ResetPomodoro(Clients) // Reset Pomodoro + } + case "pause": + if pomodoro.IsPomodoroRunning() && !pomodoro.IsPomodoroPaused() { + pomodoro.PausePomodoro() // Pause the timer + } + case "resume": + if pomodoro.IsPomodoroRunning() && pomodoro.IsPomodoroPaused() { + pomodoro.ResumePomodoro() // Resume the timer } } + } } diff --git a/internal/websocket/handle_connections.go b/internal/websocket/handle_connections.go index a7d6cfc..0b8f63e 100644 --- a/internal/websocket/handle_connections.go +++ b/internal/websocket/handle_connections.go @@ -1,14 +1,12 @@ package websocket import ( + "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" "net/http" ) -// Map to track connected clients -var Clients = make(map[*websocket.Conn]bool) - // Upgrader to upgrade HTTP requests to WebSocket connections var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, @@ -25,7 +23,9 @@ func HandleConnections(w http.ResponseWriter, r *http.Request) { defer ws.Close() // Register the new client - Clients[ws] = true + Clients[ws] = &models.Client{ + Conn: ws, // Store the WebSocket connection + } // Listen for commands from the connected client handleClientCommands(ws) diff --git a/pkg/models/types.go b/pkg/models/broadcast.go similarity index 75% rename from pkg/models/types.go rename to pkg/models/broadcast.go index ea04e21..78463b9 100644 --- a/pkg/models/types.go +++ b/pkg/models/broadcast.go @@ -7,8 +7,3 @@ type BroadcastMessage struct { MaxSession int `json:"max_session"` // Total number of sessions TimeLeft int `json:"time_left"` // Remaining time in seconds } - -// ClientCommand represents a command from the client (start/stop). -type ClientCommand struct { - Command string `json:"command"` -} diff --git a/pkg/models/client.go b/pkg/models/client.go new file mode 100644 index 0000000..8d5c3e9 --- /dev/null +++ b/pkg/models/client.go @@ -0,0 +1,30 @@ +package models + +import ( + "github.com/gorilla/websocket" + "log" + "sync" +) + +// ClientCommand represents a command from the client (start/stop). +type ClientCommand struct { + Command string `json:"command"` +} + +type Client struct { + Conn *websocket.Conn + Mutex sync.Mutex +} + +// It automatically locks and unlocks the mutex to ensure that only one goroutine can write at a time. +func (c *Client) SendMessage(messageType int, data []byte) error { + c.Mutex.Lock() + defer c.Mutex.Unlock() + + err := c.Conn.WriteMessage(messageType, data) + if err != nil { + log.Printf("Error writing to WebSocket: %v", err) + c.Conn.Close() // Close the connection on error + } + return err +}