Add basic stateful application code
This commit is contained in:
parent
fb576f3d51
commit
bf4e19378d
1 changed files with 334 additions and 0 deletions
334
application/application.go
Normal file
334
application/application.go
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// Package application is the base of a stateful application, storing loaded data in memory.
|
||||
package application
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"suruatoel.xyz/timefiles/lib"
|
||||
)
|
||||
|
||||
// Application represents a stateful time-tracker application.
|
||||
//
|
||||
// jobLock is a mutual-exclusive lock to limit the execution of asynchronous jobs.
|
||||
//
|
||||
// jobWait is a wait group to wait for asynchronous jobs to finished.
|
||||
//
|
||||
// ui represents the user interface allowing the application to call callbacks.
|
||||
//
|
||||
// projectsByID stores projects by their ID
|
||||
//
|
||||
// projectsByTitle stores projects by their title
|
||||
//
|
||||
// activitiesByID stores activities by the ID of the project they belong to and their ID
|
||||
//
|
||||
// activitiesByTitle stores activities by the ID of the project they belong to and their title
|
||||
type Application struct {
|
||||
jobLock sync.Mutex
|
||||
jobWait sync.WaitGroup
|
||||
ui UserInterface
|
||||
projectsByID map[string]*lib.Project
|
||||
projectsByTitle map[string]*lib.Project
|
||||
activitiesByID map[string]map[string]*lib.Activity
|
||||
activitiesByTitle map[string]map[string]*lib.Activity
|
||||
}
|
||||
|
||||
// UserInterface is the representation of a user interface with callback functions.
|
||||
//
|
||||
// SetTimerStart notifies the user interface that the update of the time has started.
|
||||
//
|
||||
// SetTimer sends an updated timer to the user interface. If the timer was stopped, nil will be passed.
|
||||
//
|
||||
// SetSummaryStart notifies the user interface that calculating the summary has started.
|
||||
//
|
||||
// SetSummary sends the calculated summary to the user interface.
|
||||
//
|
||||
// SetTotalsStart notifies the user interface that calculating the totals has started.
|
||||
//
|
||||
// SetTotals sends the calculated totals to the user interface.
|
||||
type UserInterface interface {
|
||||
SetTimerStart()
|
||||
SetTimer(timer *lib.Time, err error)
|
||||
SetSummaryStart()
|
||||
SetSummary(summary []*lib.Time, err error)
|
||||
SetTotalsStart()
|
||||
SetTotals(totals lib.TotalTime, err error)
|
||||
}
|
||||
|
||||
// NewApplication creates a new application for a user interface.
|
||||
func NewApplication(ui UserInterface) *Application {
|
||||
app := Application{
|
||||
ui: ui,
|
||||
projectsByID: make(map[string]*lib.Project),
|
||||
projectsByTitle: make(map[string]*lib.Project),
|
||||
activitiesByID: make(map[string]map[string]*lib.Activity),
|
||||
activitiesByTitle: make(map[string]map[string]*lib.Activity),
|
||||
}
|
||||
|
||||
return &app
|
||||
}
|
||||
|
||||
// Load loads initial application data like projects and activities.
|
||||
func (app *Application) Load() {
|
||||
projects, err := lib.GetProjects()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
app.projectsByID[project.ID] = project
|
||||
app.projectsByTitle[project.Title] = project
|
||||
|
||||
activities, activitiesErr := lib.GetActivities(project)
|
||||
if activitiesErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
activitiesByID := make(map[string]*lib.Activity)
|
||||
activitiesByTitle := make(map[string]*lib.Activity)
|
||||
for _, activity := range activities {
|
||||
activitiesByID[activity.ID] = activity
|
||||
activitiesByTitle[activity.Title] = activity
|
||||
}
|
||||
app.activitiesByID[project.ID] = activitiesByID
|
||||
app.activitiesByTitle[project.ID] = activitiesByTitle
|
||||
}
|
||||
}
|
||||
|
||||
// GetProject returns a project by its ID.
|
||||
func (app *Application) GetProject(projectID string) *lib.Project {
|
||||
return app.projectsByID[projectID]
|
||||
}
|
||||
|
||||
// GetActivity returns an activity by its ID.
|
||||
func (app *Application) GetActivity(project *lib.Project, activityID string) *lib.Activity {
|
||||
activities, found := app.activitiesByID[project.ID]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return activities[activityID]
|
||||
}
|
||||
|
||||
// SearchProjects searches for projects by comparing their title with a given query. The comparison is case-insensitive
|
||||
// and finds any project which title contains the query string.
|
||||
func (app *Application) SearchProjects(query string) (projects []*lib.Project) {
|
||||
for _, project := range app.projectsByTitle {
|
||||
if strings.Contains(strings.ToLower(project.Title), strings.ToLower(query)) {
|
||||
projects = append(projects, project)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SearchActivities searches for activities of a projects by comparing their title with a given query. The comparison is
|
||||
// case-insensitive and finds any activity which title contains the query string.
|
||||
func (app *Application) SearchActivities(projectTitle string, query string) (activities []*lib.Activity) {
|
||||
project, projectFound := app.projectsByTitle[projectTitle]
|
||||
if !projectFound {
|
||||
return
|
||||
}
|
||||
projectActivities, found := app.activitiesByTitle[project.ID]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
for _, activity := range projectActivities {
|
||||
if strings.Contains(strings.ToLower(activity.Title), strings.ToLower(query)) {
|
||||
activities = append(activities, activity)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTimer loads the current timer. This function queues the job and returns immediately. When the job is done,
|
||||
// UserInterface.SetTimer is called.
|
||||
func (app *Application) GetTimer() {
|
||||
app.runJob(app.getTimer)
|
||||
}
|
||||
|
||||
// StartTimer starts a new timer. This function queues the job and returns immediately. When the job is done,
|
||||
// UserInterface.SetTimer is called.
|
||||
func (app *Application) StartTimer() {
|
||||
app.runJob(app.startTimer)
|
||||
}
|
||||
|
||||
// UpdateTimer updates the current timer. This function queues the job and returns immediately. When the job is done,
|
||||
// UserInterface.SetTimer is called.
|
||||
func (app *Application) UpdateTimer(project string, activity string) {
|
||||
app.runJob(func() {
|
||||
app.updateTimer(project, activity)
|
||||
})
|
||||
}
|
||||
|
||||
// StopTimer stops the current timer. This function queues the job and returns immediately. When the job is done,
|
||||
// UserInterface.SetTimer is called.
|
||||
// Additionally, it gathers the time summary and calculates time totals, which will call UserInterface.SetSummary and
|
||||
// UserInterface.SetTotals.
|
||||
func (app *Application) StopTimer() {
|
||||
app.runJob(app.stopTimer)
|
||||
app.runJob(app.getTimeSummary)
|
||||
app.runJob(app.getTimeTotals)
|
||||
}
|
||||
|
||||
// GetTimeSummary gathers a list of time entries for the current day. This function queues the job and returns
|
||||
// immediately. When the job is done, UserInterface.SetSummary is called.
|
||||
func (app *Application) GetTimeSummary() {
|
||||
app.runJob(app.getTimeSummary)
|
||||
}
|
||||
|
||||
// GetTimeTotals collects the time spent per project and activity for the current day. This function queues the job and
|
||||
// returns immediately. When the job is done, UserInterface.SetTotals is called.
|
||||
func (app *Application) GetTimeTotals() {
|
||||
app.runJob(app.getTimeTotals)
|
||||
}
|
||||
|
||||
// Wait blocks until all asynchronous jobs have been completed.
|
||||
func (app *Application) Wait() {
|
||||
app.jobWait.Wait()
|
||||
}
|
||||
|
||||
// runJob runs a job in a separate go-routine.
|
||||
func (app *Application) runJob(job func()) {
|
||||
app.jobWait.Add(1)
|
||||
go func() {
|
||||
job()
|
||||
app.jobWait.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// getTimer loads the current timer. It calls the corresponding callback functions on the application.
|
||||
func (app *Application) getTimer() {
|
||||
app.ui.SetTimerStart()
|
||||
timer, err := lib.GetTimer()
|
||||
app.ui.SetTimer(timer, err)
|
||||
}
|
||||
|
||||
// startTimer starts a new timer. It calls the corresponding callback functions on the application.
|
||||
func (app *Application) startTimer() {
|
||||
app.ui.SetTimerStart()
|
||||
timer, err := lib.StartTimer()
|
||||
app.ui.SetTimer(timer, err)
|
||||
}
|
||||
|
||||
// updateTimer updates the current timer. It calls the corresponding callback functions on the application.
|
||||
func (app *Application) updateTimer(projectTitle string, activityTitle string) {
|
||||
app.ui.SetTimerStart()
|
||||
timer, err := app.updateTimerInternal(projectTitle, activityTitle)
|
||||
app.ui.SetTimer(timer, err)
|
||||
}
|
||||
|
||||
// updateTimerInternal updates the current timer.
|
||||
func (app *Application) updateTimerInternal(projectTitle string, activityTitle string) (*lib.Time, error) {
|
||||
project, projectErr := app.getProjectByTitle(projectTitle)
|
||||
if projectErr != nil {
|
||||
return nil, projectErr
|
||||
}
|
||||
|
||||
activity, activityErr := app.getActivityByTitle(project, activityTitle)
|
||||
if activityErr != nil {
|
||||
return nil, activityErr
|
||||
}
|
||||
|
||||
return lib.UpdateTimer(project, activity)
|
||||
}
|
||||
|
||||
// stopTimer stops the current timer.
|
||||
func (app *Application) stopTimer() {
|
||||
app.ui.SetTimerStart()
|
||||
timer, err := lib.StopTimer()
|
||||
app.ui.SetTimer(timer, err)
|
||||
}
|
||||
|
||||
// getTimeSummary gathers a list of time entries for the current day.
|
||||
func (app *Application) getTimeSummary() {
|
||||
app.ui.SetSummaryStart()
|
||||
summary, err := lib.GetTimeSummary()
|
||||
|
||||
app.ui.SetSummary(summary, err)
|
||||
}
|
||||
|
||||
// getTimeTotals collects the time spent per project and activity for the current day.
|
||||
func (app *Application) getTimeTotals() {
|
||||
app.ui.SetTotalsStart()
|
||||
totalTime, totalTimeErr := lib.GetTimeTotals(
|
||||
time.Now().Truncate(24*time.Hour),
|
||||
time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1).Add(-1*time.Second),
|
||||
)
|
||||
|
||||
// Resolve IDs to labels
|
||||
for _, entry := range totalTime.Totals {
|
||||
project := app.GetProject(entry.ProjectID)
|
||||
if project != nil {
|
||||
entry.Project = project.Title
|
||||
activity := app.GetActivity(project, entry.ActivityID)
|
||||
if activity != nil {
|
||||
entry.Activity = activity.Title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort entries alphabetically
|
||||
slices.SortFunc(totalTime.Totals, func(a, b *lib.TotalTimeEntry) int {
|
||||
return strings.Compare(a.Project, b.Project)
|
||||
})
|
||||
|
||||
app.ui.SetTotals(totalTime, totalTimeErr)
|
||||
}
|
||||
|
||||
// getProjectByTitle gets a project by its title. If no project with the given title exists, it creates a new project.
|
||||
func (app *Application) getProjectByTitle(title string) (*lib.Project, error) {
|
||||
project, found := app.projectsByTitle[title]
|
||||
if found {
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// Create new project
|
||||
project = lib.NewProject()
|
||||
project.Title = title
|
||||
err := lib.SetProject(project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update in-memory storage
|
||||
app.projectsByID[project.ID] = project
|
||||
app.projectsByTitle[project.Title] = project
|
||||
app.activitiesByID[project.ID] = make(map[string]*lib.Activity)
|
||||
app.activitiesByTitle[project.ID] = make(map[string]*lib.Activity)
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
// getActivityByTitle gets an activity by its title. If no activity with the given title exists, it creates a new
|
||||
// activity.
|
||||
func (app *Application) getActivityByTitle(project *lib.Project, title string) (*lib.Activity, error) {
|
||||
activitiesByTitle, activitiesFound := app.activitiesByTitle[project.ID]
|
||||
if !activitiesFound {
|
||||
// This should never happen
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
activity, found := activitiesByTitle[title]
|
||||
if found {
|
||||
return activity, nil
|
||||
}
|
||||
|
||||
// Create new activity
|
||||
activity = &lib.Activity{
|
||||
Title: title,
|
||||
}
|
||||
err := lib.SetActivity(project, activity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update in-memory storage
|
||||
app.activitiesByID[project.ID][activity.ID] = activity
|
||||
app.activitiesByTitle[project.ID][activity.Title] = activity
|
||||
|
||||
return activity, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue