1
+ // Core UI logic and components
2
+ namespace TaskTimeTracker
3
+
4
+ open Avalonia
5
+ open Avalonia.Controls
6
+ open Avalonia.Controls .Primitives
7
+ open Avalonia.FuncUI
8
+ open Avalonia.FuncUI .DSL
9
+ open Avalonia.Layout
10
+ open System
11
+ open System.Timers
12
+
13
+ module Main =
14
+ let view () =
15
+ Component( fun ctx ->
16
+ // States
17
+ let runningTaskIndex = ctx.useState None
18
+ let tasks = ctx.useState ( Persistence.loadTasksFromFile())
19
+ let timerState = ctx.useState< Timer option>( None)
20
+ let autoSaveTimer = new Timer( 60000.0 )
21
+ autoSaveTimer.AutoReset <- true
22
+
23
+ // Timer handler for task time tracking
24
+ let timerHandler = ElapsedEventHandler( fun _ _ ->
25
+ match runningTaskIndex.Current with
26
+ | Some index ->
27
+ tasks.Set(
28
+ tasks.Current
29
+ |> List.mapi ( fun i ( name , elapsedTime ) ->
30
+ if i = index then ( name, elapsedTime + TimeSpan.FromSeconds( 1.0 ))
31
+ else ( name, elapsedTime)
32
+ )
33
+ )
34
+ | None ->
35
+ match timerState.Current with
36
+ | Some t -> t.Enabled <- false
37
+ | None -> ()
38
+ )
39
+
40
+ // Timer handler for auto-saving tasks
41
+ let autoSaveHandler = ElapsedEventHandler( fun _ _ ->
42
+ Persistence.saveTasksToFile tasks.Current
43
+ )
44
+
45
+ autoSaveTimer.Elapsed.AddHandler( autoSaveHandler)
46
+ autoSaveTimer.Start()
47
+
48
+ // Create Timer once and store it in state
49
+ ctx.useEffect(
50
+ ( fun () ->
51
+ let timer = new Timer( 1000.0 )
52
+ timer.AutoReset <- true
53
+ timer.Elapsed.AddHandler( timerHandler)
54
+ timerState.Set( Some timer)
55
+ { new IDisposable with
56
+ member _. Dispose() =
57
+ timer.Stop()
58
+ timer.Elapsed.RemoveHandler( timerHandler)
59
+ timer.Dispose()
60
+ autoSaveTimer.Stop()
61
+ autoSaveTimer.Elapsed.RemoveHandler( autoSaveHandler)
62
+ Persistence.saveTasksToFile tasks.Current
63
+ }
64
+ ),
65
+ [ EffectTrigger.AfterInit ]
66
+ )
67
+
68
+ let removeTask index =
69
+ tasks.Set( tasks.Current |> List.indexed |> List.filter ( fun ( i , _ ) -> i <> index) |> List.map snd)
70
+ match runningTaskIndex.Current with
71
+ | Some i when i = index ->
72
+ runningTaskIndex.Set( None)
73
+ match timerState.Current with
74
+ | Some t -> t.Enabled <- false
75
+ | None -> ()
76
+ | _ -> ()
77
+
78
+ let addTask () =
79
+ tasks.Set( tasks.Current @ [( " New task" , TimeSpan.Zero)])
80
+
81
+ let resetTimers () =
82
+ tasks.Set( tasks.Current |> List.map ( fun ( n , _ ) -> ( n, TimeSpan.Zero)))
83
+
84
+ let togglePlayPause index =
85
+ match runningTaskIndex.Current with
86
+ | Some i when i = index ->
87
+ runningTaskIndex.Set( None)
88
+ timerState.Current |> Option.iter ( fun t -> t.Enabled <- false )
89
+ | _ ->
90
+ runningTaskIndex.Set( Some index)
91
+ timerState.Current |> Option.iter ( fun t -> t.Stop(); t.Start())
92
+
93
+ // Calculate total elapsed time
94
+ let totalElapsedTime =
95
+ tasks.Current
96
+ |> List.fold ( fun acc ( _ , elapsedTime ) -> acc + elapsedTime) TimeSpan.Zero
97
+
98
+ DockPanel.create [
99
+ DockPanel.children [
100
+ ScrollViewer.create [
101
+ ScrollViewer.verticalScrollBarVisibility ScrollBarVisibility.Auto
102
+ ScrollViewer.content (
103
+ StackPanel.create [
104
+ StackPanel.margin ( Thickness( 10.0 ))
105
+ StackPanel.children (
106
+ ( tasks.Current |> List.indexed |> List.map ( fun ( index , ( task , elapsedTime )) ->
107
+ Grid.create [
108
+ Grid.columnDefinitions " 50, *, 50, 100"
109
+ Grid.horizontalAlignment HorizontalAlignment.Stretch
110
+ Grid.children [
111
+ Button.create [
112
+ Button.content " 🚮"
113
+ Button.margin ( Thickness( 0.0 , 0.0 , 10.0 , 0.0 ))
114
+ Button.onClick ( fun _ -> removeTask index)
115
+ Grid.column 0
116
+ ]
117
+ TextBox.create [
118
+ TextBox.text task
119
+ TextBox.fontSize 18.0
120
+ TextBox.margin ( Thickness( 0.0 , 5.0 ))
121
+ TextBox.horizontalAlignment HorizontalAlignment.Stretch
122
+ TextBox.onTextChanged ( fun newText ->
123
+ tasks.Set(
124
+ tasks.Current |> List.mapi ( fun i ( n , t ) ->
125
+ if i = index then ( newText, t) else ( n, t)
126
+ )
127
+ )
128
+ )
129
+ TextBox.onGotFocus ( fun args ->
130
+ let textBox = args.Source :?> TextBox
131
+ if textBox.Text = " New task" then textBox.Clear()
132
+ )
133
+ Grid.column 1
134
+ ]
135
+ Button.create [
136
+ Button.content ( if runningTaskIndex.Current = Some index then " ⏸️" else " ⏯️" )
137
+ Button.margin ( Thickness( 5.0 , 0.0 , 0.0 , 0.0 ))
138
+ Button.horizontalAlignment HorizontalAlignment.Right
139
+ Button.onClick ( fun _ -> togglePlayPause index)
140
+ Grid.column 2
141
+ ]
142
+ TextBlock.create [
143
+ TextBlock.text $" %02d {elapsedTime.Hours}:%02d {elapsedTime.Minutes}:%02d {elapsedTime.Seconds}"
144
+ TextBlock.margin ( Thickness( 10.0 , 0.0 , 10.0 , 0.0 ))
145
+ TextBlock.fontSize 18.0
146
+ TextBlock.verticalAlignment VerticalAlignment.Center
147
+ TextBlock.horizontalAlignment HorizontalAlignment.Right
148
+ Grid.column 3
149
+ ]
150
+ ]
151
+ ]
152
+ )) @ [
153
+ Grid.create [
154
+ Grid.columnDefinitions " auto, *"
155
+ Grid.children [
156
+ StackPanel.create [
157
+ StackPanel.orientation Orientation.Horizontal
158
+ StackPanel.children [
159
+ Button.create [
160
+ Button.content " 🆕"
161
+ Button.margin ( Thickness( 0.0 , 10.0 , 10.0 , 0.0 ))
162
+ Button.horizontalAlignment HorizontalAlignment.Left
163
+ Button.onClick ( fun _ -> addTask ())
164
+ ]
165
+ Button.create [
166
+ Button.content " 🔄"
167
+ Button.margin ( Thickness( 0.0 , 10.0 , 10.0 , 0.0 ))
168
+ Button.horizontalAlignment HorizontalAlignment.Left
169
+ Button.onClick ( fun _ -> resetTimers ())
170
+ ]
171
+ TextBlock.create [
172
+ TextBlock.text $" Total time: %02d {totalElapsedTime.Hours}:%02d {totalElapsedTime.Minutes}:%02d {totalElapsedTime.Seconds}"
173
+ TextBlock.fontSize 18.0
174
+ TextBlock.verticalAlignment VerticalAlignment.Center
175
+ TextBlock.horizontalAlignment HorizontalAlignment.Left
176
+ ]
177
+ ]
178
+ Grid.column 0
179
+ Grid.columnSpan 2
180
+ ]
181
+ ]
182
+ ]
183
+ ]
184
+ )
185
+ ]
186
+ )
187
+ ]
188
+ ]
189
+ ]
190
+ )
0 commit comments