Skip to content

Commit 4292c0e

Browse files
committed
yolo bro
1 parent c92112c commit 4292c0e

File tree

8 files changed

+339
-13
lines changed

8 files changed

+339
-13
lines changed

App.fs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Application configuration and theme setup
2+
namespace TaskTimeTracker
3+
4+
open Avalonia
5+
open Avalonia.Controls.ApplicationLifetimes
6+
open Avalonia.Themes.Fluent
7+
8+
type App() =
9+
inherit Application()
10+
11+
override this.Initialize() =
12+
this.Styles.Add (FluentTheme())
13+
this.RequestedThemeVariant <- Styling.ThemeVariant.Default
14+
15+
override this.OnFrameworkInitializationCompleted() =
16+
match this.ApplicationLifetime with
17+
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
18+
desktopLifetime.MainWindow <- MainWindow()
19+
| _ -> ()

Main.fs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
)

MainWIndow.fs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Main window definition
2+
namespace TaskTimeTracker
3+
4+
open Avalonia.FuncUI.Hosts
5+
open TaskTimeTracker
6+
7+
type MainWindow() =
8+
inherit HostWindow()
9+
do
10+
base.Title <- "Task Time Tracker"
11+
base.MinWidth <- 600.0
12+
base.MinHeight <- 400.0
13+
base.Width <- 600.0
14+
base.Height <- 400.0
15+
base.MaxWidth <- 600.0
16+
base.MaxHeight <- 400.0
17+
let view = Main.view()
18+
base.Content <- view
19+

Persistence.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Saving and loading tasks to/from JSON
2+
namespace TaskTimeTracker
3+
4+
open System
5+
open System.IO
6+
open System.Text.Json
7+
8+
module Persistence =
9+
let filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "task-time-tracker", "tasks.json")
10+
11+
let saveTasksToFile tasks =
12+
let directory = Path.GetDirectoryName(filePath)
13+
if not (Directory.Exists(directory)) then
14+
Directory.CreateDirectory(directory) |> ignore
15+
let json = JsonSerializer.Serialize(tasks)
16+
File.WriteAllText(filePath, json)
17+
18+
let loadTasksFromFile () =
19+
if File.Exists(filePath) then
20+
let json = File.ReadAllText(filePath)
21+
JsonSerializer.Deserialize<(string * TimeSpan) list>(json)
22+
else
23+
[("New task", TimeSpan.Zero)]
24+

Program.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Entry point
2+
namespace TaskTimeTracker
3+
4+
open Avalonia
5+
6+
module Program =
7+
[<EntryPoint>]
8+
let main(args: string[]) =
9+
AppBuilder
10+
.Configure<App>()
11+
.UsePlatformDetect()
12+
.UseSkia()
13+
.StartWithClassicDesktopLifetime(args)

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11

2-
![GitHub repo size](https://img.shields.io/github/repo-size/MaxGripe/repository-template)
3-
![GitHub License](https://img.shields.io/github/license/MaxGripe/repository-template)
4-
![GitHub Created At](https://img.shields.io/github/created-at/MaxGripe/repository-template)
5-
![GitHub forks](https://img.shields.io/github/forks/MaxGripe/repository-template)
6-
![GitHub Repo stars](https://img.shields.io/github/stars/MaxGripe/repository-template)
2+
![GitHub repo size](https://img.shields.io/github/repo-size/The-TTT-Syndicate/task-time-tracker-fsharp)
3+
![GitHub License](https://img.shields.io/github/license/The-TTT-Syndicate/task-time-tracker-fsharp)
4+
![GitHub Created At](https://img.shields.io/github/created-at/The-TTT-Syndicate/task-time-tracker-fsharp)
5+
![GitHub forks](https://img.shields.io/github/forks/The-TTT-Syndicate/task-time-tracker-fsharp)
6+
![GitHub Repo stars](https://img.shields.io/github/The-TTT-Syndicate/task-time-tracker-fsharp)
77

8+
# Task Time Tracker in F#
89

9-
# Template Repository
10+
A simple time-tracking tool. Works fine on Windows, but on macOS... not so much.
1011

11-
This repository serves as a template for starting new projects.
12+
![task-time-tracker-fsharp.png](task-time-tracker-fsharp.png)
1213

13-
## Features
14+
## Information
1415

15-
- Easy setup
16-
- Customizable configurations
17-
- Some popular badges already in place
16+
This project was an attempt to create a UI using F#. I have to say, it has been a truly terrible and frustrating experience. While I did manage to put together a basic application, it was far from easy.
1817

19-
## Getting started
18+
Back when we were just starting to learn programming in our first semester of IT studies, we could build similar UIs effortlessly using Windows.Forms and a GUI editor in Visual Studio. It was simple and straightforward. Now, making a UI feels like pure suffering.
2019

21-
To use this template, select it from the **Repository template** dropdown when creating a new repository.
20+
## Contribution
21+
22+
On macOS, the program crashes after running for a while, but honestly, I have no interest in debugging it. If someone feels like tackling this issue, go for it! I'll buy a good craft beer for whoever fixes the bug. As for me, I'm done with it.
2223

2324
## License
2425

task-time-tracker-fsharp.png

47.4 KB
Loading

task-time-tracker.fsproj

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>WinExe</OutputType>
5+
<UseAppHost>true</UseAppHost>
6+
<TargetFramework>net9.0</TargetFramework>
7+
<RootNamespace>task_time_tracker</RootNamespace>
8+
<PublishSingleFile>true</PublishSingleFile>
9+
<PublishReadyToRun>false</PublishReadyToRun>
10+
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
11+
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
12+
<InvariantGlobalization>true</InvariantGlobalization>
13+
<PublishTrimmed>false</PublishTrimmed>
14+
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
15+
<DebuggerSupport>false</DebuggerSupport>
16+
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
17+
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
18+
<EventSourceSupport>false</EventSourceSupport>
19+
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
20+
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
21+
<MetricsSupport>false</MetricsSupport>
22+
<StackTraceSupport>false</StackTraceSupport>
23+
<NullabilityInfoContextSupport>false</NullabilityInfoContextSupport>
24+
<UseSystemResourceKeys>false</UseSystemResourceKeys>
25+
<IlcDisableReflection>false</IlcDisableReflection>
26+
<IlcGenerateCompleteTypeMetadata>false</IlcGenerateCompleteTypeMetadata>
27+
<IlcOptimizeAssembly>true</IlcOptimizeAssembly>
28+
<XmlResolverIsNetworkingEnabledByDefault>false</XmlResolverIsNetworkingEnabledByDefault>
29+
<WarningLevel>0</WarningLevel>
30+
<DebugType>none</DebugType>
31+
<OptimizationPreference>Size</OptimizationPreference>
32+
<SelfContained>true</SelfContained>
33+
<EnableDynamicCode>true</EnableDynamicCode>
34+
<EnableRuntimeMarshalling>true</EnableRuntimeMarshalling>
35+
<UseSystemResourceKeys>false</UseSystemResourceKeys>
36+
<EventSourceSupport>false</EventSourceSupport>
37+
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
38+
<StackTraceSupport>false</StackTraceSupport>
39+
<NullabilityInfoContextSupport>false</NullabilityInfoContextSupport>
40+
<StripSymbols>true</StripSymbols>
41+
<Configuration>Release</Configuration>
42+
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
43+
<Optimize>true</Optimize>
44+
</PropertyGroup>
45+
46+
<ItemGroup>
47+
<Compile Include="Persistence.fs" />
48+
<Compile Include="Main.fs" />
49+
<Compile Include="MainWindow.fs" />
50+
<Compile Include="App.fs" />
51+
<Compile Include="Program.fs" />
52+
</ItemGroup>
53+
54+
<ItemGroup>
55+
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
56+
<PackageReference Include="Avalonia.FuncUI" Version="1.5.1" />
57+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
58+
</ItemGroup>
59+
60+
</Project>

0 commit comments

Comments
 (0)