Compare commits

..

19 Commits

Author SHA1 Message Date
320d6ceb0b remove native dialogs 2023-04-30 18:48:31 +02:00
8fd89c3b4c remove native dialogs 2023-04-30 18:47:36 +02:00
467cec1c30 use native dialogs 2023-04-26 01:10:17 +02:00
8888572829 ensure paths work on MacOS 2023-04-26 00:31:12 +02:00
f94a0e89d6 whiten tree font 2023-04-26 00:21:59 +02:00
24ae05c425 allow time edits 2023-04-26 00:19:35 +02:00
0b39a3fa5c add license 2023-04-23 13:14:34 +02:00
7b6b3feb56 better readme 2023-04-23 12:51:41 +02:00
126df5317f better button 2023-04-23 01:07:22 +02:00
17e9a59f3c exports 2023-04-23 00:50:24 +02:00
f37e3050b5 set up exports 2023-04-22 23:22:19 +02:00
aea91c6261 cli command 2023-04-22 23:06:27 +02:00
b69be6e07c added theme 2023-04-22 22:02:31 +02:00
cbc64a0b02 more clean up 2023-04-22 18:45:35 +02:00
397bc23ff5 save position 2023-04-22 04:52:00 +02:00
cf171bcc63 cleaned up 2023-04-22 04:04:35 +02:00
17d8637156 bugs squished 2023-04-22 03:08:05 +02:00
24c4b98a5e tasks list working 2023-03-10 03:25:50 +04:00
66b25bae97 convert to Godot 3.5 2023-03-10 00:26:57 +04:00
57 changed files with 1965 additions and 983 deletions

24
.gitignore vendored
View File

@ -1,2 +1,22 @@
.godot/ # .gitignore generated by Hourglass
exports/ # For more information on what files should be excluded from version control,
# see <https://docs.godotengine.org/en/stable/getting_started/workflow/project_setup/version_control_systems.html#files-to-exclude-from-vcs>
.import/
export.cfg
exports/*.apk
exports/*.exe
exports/*.pck
exports/*.x86_64
exports/*.zip
exports/web/
# Exclude imported translations
*.translation
# Mono-specific
.mono/
data_*/
# Don't include those pesky .DS_Store files on macOS
.DS_Store

View File

@ -1,14 +1,15 @@
# Rat Times # ![Logo representing a rat's butt](assets/logo.png) Rat Times
Track your time, save it to a CSV file. Track your time, save it to a CSV file.
![screenshot of the app](info/screenshot.png) Uses Godot 3.5
Uses Godot 4 ![screenshot of the app in usage](exports/screenshot.png)
## Features ## Features
- Saves to custom CSV file. Sync this file with syncthing/nextcloud/dropbox/whatever you like - Saves to custom CSV file. Sync this file with syncthing/nextcloud/dropbox/whatever you like
- File watcher. Change the CSV externally, see the update in the UI
- Versions for Linux, Windows, Android, and presumably IOS and Mac (untested) - Versions for Linux, Windows, Android, and presumably IOS and Mac (untested)
- Custom theming possible - Custom theming possible
- Does not depend on a timer, so you can close the app immediately after starting a task - Does not depend on a timer, so you can close the app immediately after starting a task

View File

@ -1,33 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://xixlsyswj6mv"
path="res://.godot/imported/Cairo-VariableFont_slnt,wght.ttf-8e1ad3a3f88c2663b83086e73553b314.fontdata"
[deps]
source_file="res://assets/Cairo-VariableFont_slnt,wght.ttf"
dest_files=["res://.godot/imported/Cairo-VariableFont_slnt,wght.ttf-8e1ad3a3f88c2663b83086e73553b314.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://cpmyyivxx0dlt" path="res://.import/logo.png-e2220799298e3631eb0e245316e0501a.stex"
path="res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,24 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/logo.png" source_file="res://assets/logo.png"
dest_files=["res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"] dest_files=[ "res://.import/logo.png-e2220799298e3631eb0e245316e0501a.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1 svg/scale=1.0

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://fk5s6m8qlsei" path="res://.import/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.stex"
path="res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,27 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/logo.svg" source_file="res://assets/logo.svg"
dest_files=["res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"] dest_files=[ "res://.import/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://bcledyqebmri" path="res://.import/play.svg-1c68bc58d294f89383fc6b13dae7f4f1.stex"
path="res://.godot/imported/play.svg-1c68bc58d294f89383fc6b13dae7f4f1.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,27 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/play.svg" source_file="res://assets/play.svg"
dest_files=["res://.godot/imported/play.svg-1c68bc58d294f89383fc6b13dae7f4f1.ctex"] dest_files=[ "res://.import/play.svg-1c68bc58d294f89383fc6b13dae7f4f1.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

54
assets/play_small.svg Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="13"
height="13"
viewBox="0 0 3.4395833 3.4395833"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="play_small.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="35.356609"
inkscape:cx="6.3495909"
inkscape:cy="5.8263506"
inkscape:window-width="1896"
inkscape:window-height="1029"
inkscape:window-x="12"
inkscape:window-y="39"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid7036"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="rect11457"
style="fill:#ffffff;stroke-width:1.85208;paint-order:stroke markers fill;stop-color:#000000"
d="M 0.26458333,0.26458333 3.175,1.7197917 0.26458333,3.175 Z"
sodipodi:nodetypes="cccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/play_small.svg-6ecf1cf55097c1673c0917a7e7624a3c.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/play_small.svg"
dest_files=[ "res://.import/play_small.svg-6ecf1cf55097c1673c0917a7e7624a3c.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View File

@ -1,19 +1,15 @@
[remap] [remap]
importer="oggvorbisstr" importer="ogg_vorbis"
type="AudioStreamOggVorbis" type="AudioStreamOGGVorbis"
uid="uid://cdsbhoidgyx70" path="res://.import/pop.ogg-1ea32f2d825ac1e6d9420f3fecda89df.oggstr"
path="res://.godot/imported/pop.ogg-1ea32f2d825ac1e6d9420f3fecda89df.oggvorbisstr"
[deps] [deps]
source_file="res://assets/pop.ogg" source_file="res://assets/pop.ogg"
dest_files=["res://.godot/imported/pop.ogg-1ea32f2d825ac1e6d9420f3fecda89df.oggvorbisstr"] dest_files=[ "res://.import/pop.ogg-1ea32f2d825ac1e6d9420f3fecda89df.oggstr" ]
[params] [params]
loop=false loop=false
loop_offset=0 loop_offset=0
bpm=0
beat_count=0
bar_beats=4

BIN
assets/rcedit-x64.exe Normal file

Binary file not shown.

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://c56y8w3k75rxc" path="res://.import/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.stex"
path="res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,27 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/stop.svg" source_file="res://assets/stop.svg"
dest_files=["res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"] dest_files=[ "res://.import/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

57
assets/stop_small.svg Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22"
height="22"
viewBox="0 0 5.8208333 5.8208333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="stop_small.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#323232"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="35.356609"
inkscape:cx="12.402208"
inkscape:cy="11.115319"
inkscape:window-width="1896"
inkscape:window-height="1029"
inkscape:window-x="12"
inkscape:window-y="39"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid7036"
originx="0"
originy="0"
empspacing="8" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:0.994836;stroke-width:4;stroke-linecap:round;stroke-opacity:0;paint-order:stroke markers fill;stop-color:#000000"
id="rect341"
width="4.7624998"
height="4.7624998"
x="0.52916664"
y="0.5291667" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/stop_small.svg-d7819f3a82ff689d7390798487abb123.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/stop_small.svg"
dest_files=[ "res://.import/stop_small.svg-d7819f3a82ff689d7390798487abb123.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=0.6

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://ctxkpfc0syh2m" path="res://.import/tasks.svg-78ebb4f2e18a1507072d2a7762176d95.stex"
path="res://.godot/imported/tasks.svg-78ebb4f2e18a1507072d2a7762176d95.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,27 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/tasks.svg" source_file="res://assets/tasks.svg"
dest_files=["res://.godot/imported/tasks.svg-78ebb4f2e18a1507072d2a7762176d95.ctex"] dest_files=[ "res://.import/tasks.svg-78ebb4f2e18a1507072d2a7762176d95.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="13"
height="13"
viewBox="0 0 3.4395833 3.4395833"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="toggle_button.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="50.001796"
inkscape:cx="6.3597716"
inkscape:cy="6.4797673"
inkscape:window-width="1896"
inkscape:window-height="1029"
inkscape:window-x="12"
inkscape:window-y="39"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid7036"
originx="0"
originy="0" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="rect11457"
style="fill:#ffffff;stroke-width:1.85208;paint-order:stroke markers fill;stop-color:#000000"
d="M 1.7197916 0.79374999 L 0.26458333 2.1166666 L 0.84645995 2.1166666 L 1.7197916 1.3229167 L 2.5931233 2.1166666 L 3.175 2.1166666 L 1.7197916 0.79374999 z " />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/toggle_button_up.svg-42d50bf036f845a14cc3c73e27b9d2ba.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/toggle_button_up.svg"
dest_files=[ "res://.import/toggle_button_up.svg-42d50bf036f845a14cc3c73e27b9d2ba.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View File

@ -1,9 +1,8 @@
[remap] [remap]
importer="texture" importer="texture"
type="CompressedTexture2D" type="StreamTexture"
uid="uid://dskgsrua03gwe" path="res://.import/wheel.svg-6ab291b0608b06b66eec00e4d4332248.stex"
path="res://.godot/imported/wheel.svg-6ab291b0608b06b66eec00e4d4332248.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
@ -11,27 +10,26 @@ metadata={
[deps] [deps]
source_file="res://assets/wheel.svg" source_file="res://assets/wheel.svg"
dest_files=["res://.godot/imported/wheel.svg-6ab291b0608b06b66eec00e4d4332248.ctex"] dest_files=[ "res://.import/wheel.svg-6ab291b0608b06b66eec00e4d4332248.stex" ]
[params] [params]
compress/mode=0 compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/hdr_compression=1 compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 flags/repeat=0
mipmaps/generate=false flags/filter=true
mipmaps/limit=-1 flags/mipmaps=false
roughness/mode=0 flags/anisotropic=false
roughness/src_normal="" flags/srgb=2
process/fix_alpha_border=true process/fix_alpha_border=true
process/premult_alpha=false process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false process/normal_map_invert_y=false
process/hdr_as_srgb=false stream=false
process/hdr_clamp_exposure=false size_limit=0
process/size_limit=0 detect_3d=true
detect_3d/compress_to=1
svg/scale=1.0 svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

BIN
build/logo.icns Normal file

Binary file not shown.

BIN
build/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="ConfigManager" load_steps=2 format=3] [gd_resource type="Resource" load_steps=2 format=2]
[ext_resource type="Script" path="res://scripts/config_manager.gd" id="1_xfu8y"] [ext_resource path="res://scripts/config_manager.gd" type="Script" id=1]
[resource] [resource]
script = ExtResource("1_xfu8y") script = ExtResource( 1 )

7
default_env.tres Normal file
View File

@ -0,0 +1,7 @@
[gd_resource type="Environment" load_steps=2 format=2]
[sub_resource type="ProceduralSky" id=1]
[resource]
background_mode = 2
background_sky = SubResource( 1 )

View File

@ -1,81 +1,81 @@
[preset.0] [preset.0]
name="Linux/X11" name="HTML5"
platform="Linux/X11" platform="HTML5"
runnable=true runnable=true
dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="exports/rat-times-x64.x86_64" export_path="exports/web/index.html"
encryption_include_filters="" script_export_mode=1
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key="" script_encryption_key=""
[preset.0.options] [preset.0.options]
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
debug/export_console_script=1 variant/export_type=0
binary_format/embed_pck=false vram_texture_compression/for_desktop=true
texture_format/bptc=false vram_texture_compression/for_mobile=false
texture_format/s3tc=true html/export_icon=true
texture_format/etc=false html/custom_html_shell=""
texture_format/etc2=false html/head_include=""
binary_format/architecture="x86_64" html/canvas_resize_policy=2
ssh_remote_deploy/enabled=false html/focus_canvas_on_start=true
ssh_remote_deploy/host="user@host_ip" html/experimental_virtual_keyboard=false
ssh_remote_deploy/port="22" progressive_web_app/enabled=false
ssh_remote_deploy/extra_args_ssh="" progressive_web_app/offline_page=""
ssh_remote_deploy/extra_args_scp="" progressive_web_app/display=1
ssh_remote_deploy/run_script="#!/usr/bin/env bash progressive_web_app/orientation=0
export DISPLAY=:0 progressive_web_app/icon_144x144=""
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" progressive_web_app/icon_180x180=""
\"{temp_dir}/{exe_name}\" {cmd_args}" progressive_web_app/icon_512x512=""
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash progressive_web_app/background_color=Color( 0, 0, 0, 1 )
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
[preset.1] [preset.1]
name="macOS" name="Mac OSX"
platform="macOS" platform="Mac OSX"
runnable=true runnable=true
dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="exports/Rat Times.zip" export_path="exports/rat-times.zip"
encryption_include_filters="" script_export_mode=1
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key="" script_encryption_key=""
[preset.1.options] [preset.1.options]
binary_format/architecture="universal"
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
debug/export_console_script=1 application/name="Rat Times"
application/icon="res://logo.icns" application/info="Made with Godot Engine"
application/icon_interpolation=4 application/icon="res://assets/logo.icns"
application/bundle_identifier="org.mutnt.io.rat-times" application/identifier="io.mutnt.app.rattimes"
application/signature="" application/signature=""
application/app_category="Developer-tools" application/app_category="Games"
application/short_version="1.0" application/short_version="1.0"
application/version="1.0" application/version="1.0"
application/copyright="" application/copyright=""
application/copyright_localized={} display/high_res=false
display/high_res=true privacy/microphone_usage_description=""
codesign/codesign=1 privacy/camera_usage_description=""
privacy/location_usage_description=""
privacy/address_book_usage_description=""
privacy/calendar_usage_description=""
privacy/photos_library_usage_description=""
privacy/desktop_folder_usage_description=""
privacy/documents_folder_usage_description="Documents required to save file"
privacy/downloads_folder_usage_description=""
privacy/network_volumes_usage_description=""
privacy/removable_volumes_usage_description=""
codesign/enable=true
codesign/identity="" codesign/identity=""
codesign/certificate_file="" codesign/timestamp=true
codesign/certificate_password="" codesign/hardened_runtime=true
codesign/replace_existing_signature=true
codesign/entitlements/custom_file="" codesign/entitlements/custom_file=""
codesign/entitlements/allow_jit_code_execution=false codesign/entitlements/allow_jit_code_execution=false
codesign/entitlements/allow_unsigned_executable_memory=false codesign/entitlements/allow_unsigned_executable_memory=false
@ -98,75 +98,105 @@ codesign/entitlements/app_sandbox/files_downloads=0
codesign/entitlements/app_sandbox/files_pictures=0 codesign/entitlements/app_sandbox/files_pictures=0
codesign/entitlements/app_sandbox/files_music=0 codesign/entitlements/app_sandbox/files_music=0
codesign/entitlements/app_sandbox/files_movies=0 codesign/entitlements/app_sandbox/files_movies=0
codesign/entitlements/app_sandbox/helper_executables=[] codesign/custom_options=PoolStringArray( )
codesign/custom_options=PackedStringArray() notarization/enable=false
notarization/notarization=0
notarization/apple_id_name="" notarization/apple_id_name=""
notarization/apple_id_password="" notarization/apple_id_password=""
notarization/apple_team_id="" notarization/apple_team_id=""
notarization/api_uuid="" texture_format/s3tc=true
notarization/api_key="" texture_format/etc=false
notarization/api_key_id="" texture_format/etc2=false
privacy/microphone_usage_description=""
privacy/microphone_usage_description_localized={}
privacy/camera_usage_description=""
privacy/camera_usage_description_localized={}
privacy/location_usage_description=""
privacy/location_usage_description_localized={}
privacy/address_book_usage_description=""
privacy/address_book_usage_description_localized={}
privacy/calendar_usage_description=""
privacy/calendar_usage_description_localized={}
privacy/photos_library_usage_description=""
privacy/photos_library_usage_description_localized={}
privacy/desktop_folder_usage_description=""
privacy/desktop_folder_usage_description_localized={}
privacy/documents_folder_usage_description=""
privacy/documents_folder_usage_description_localized={}
privacy/downloads_folder_usage_description=""
privacy/downloads_folder_usage_description_localized={}
privacy/network_volumes_usage_description=""
privacy/network_volumes_usage_description_localized={}
privacy/removable_volumes_usage_description=""
privacy/removable_volumes_usage_description_localized={}
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
[preset.2] [preset.2]
name="Android" name="Linux/X11"
platform="Android" platform="Linux/X11"
runnable=true runnable=true
dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="exports/rat-times-x64.apk" export_path="exports/rat-times.x86_64"
encryption_include_filters="" script_export_mode=1
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key="" script_encryption_key=""
[preset.2.options] [preset.2.options]
custom_template/debug="" custom_template/debug=""
custom_template/release="" custom_template/release=""
gradle_build/use_gradle_build=false binary_format/64_bits=true
gradle_build/export_format=0 binary_format/embed_pck=false
gradle_build/min_sdk="" texture_format/bptc=false
gradle_build/target_sdk="" texture_format/s3tc=true
architectures/armeabi-v7a=false texture_format/etc=false
texture_format/etc2=false
texture_format/no_bptc_fallbacks=true
[preset.3]
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.exe"
script_export_mode=1
script_encryption_key=""
[preset.3.options]
custom_template/debug=""
custom_template/release=""
binary_format/64_bits=true
binary_format/embed_pck=false
texture_format/bptc=false
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
texture_format/no_bptc_fallbacks=true
codesign/enable=false
codesign/identity_type=0
codesign/identity=""
codesign/password=""
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PoolStringArray( )
application/modify_resources=true
application/icon="res://assets/logo.ico"
application/file_version=""
application/product_version=""
application/company_name="Mutnt"
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
[preset.4]
name="Android"
platform="Android"
runnable=true
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.apk"
script_export_mode=1
script_encryption_key=""
[preset.4.options]
custom_template/debug=""
custom_template/release=""
custom_build/use_custom_build=false
custom_build/export_format=0
custom_build/min_sdk=""
custom_build/target_sdk=""
architectures/armeabi-v7a=true
architectures/arm64-v8a=true architectures/arm64-v8a=true
architectures/x86=false architectures/x86=false
architectures/x86_64=false architectures/x86_64=false
@ -176,12 +206,13 @@ keystore/debug_password="android"
keystore/release="" keystore/release=""
keystore/release_user="" keystore/release_user=""
keystore/release_password="" keystore/release_password=""
one_click_deploy/clear_previous_install=false
version/code=1 version/code=1
version/name="1.0" version/name="1.0"
package/unique_name="org.mutnt.io.ratstimes" package/unique_name="io.mutnt.app.$genname"
package/name="" package/name=""
package/signed=true package/signed=true
package/app_category=2 package/classify_as_game=true
package/retain_data_on_uninstall=false package/retain_data_on_uninstall=false
package/exclude_from_recents=false package/exclude_from_recents=false
launcher_icons/main_192x192="" launcher_icons/main_192x192=""
@ -202,7 +233,7 @@ command_line/extra_args=""
apk_expansion/enable=false apk_expansion/enable=false
apk_expansion/SALT="" apk_expansion/SALT=""
apk_expansion/public_key="" apk_expansion/public_key=""
permissions/custom_permissions=PackedStringArray() permissions/custom_permissions=PoolStringArray( )
permissions/access_checkin_properties=false permissions/access_checkin_properties=false
permissions/access_coarse_location=false permissions/access_coarse_location=false
permissions/access_fine_location=false permissions/access_fine_location=false
@ -349,145 +380,3 @@ permissions/write_sms=false
permissions/write_social_stream=false permissions/write_social_stream=false
permissions/write_sync_settings=false permissions/write_sync_settings=false
permissions/write_user_dictionary=false permissions/write_user_dictionary=false
[preset.3]
name="iOS"
platform="iOS"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.ipa"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key=""
[preset.3.options]
custom_template/debug=""
custom_template/release=""
architectures/arm64=true
application/app_store_team_id="org.mutnt.io"
application/provisioning_profile_uuid_debug=""
application/code_sign_identity_debug=""
application/export_method_debug=1
application/provisioning_profile_uuid_release=""
application/code_sign_identity_release=""
application/export_method_release=0
application/targeted_device_family=2
application/bundle_identifier="rat-times"
application/signature=""
application/short_version="1.0"
application/version="1.0"
application/icon_interpolation=4
application/launch_screens_interpolation=4
capabilities/access_wifi=false
capabilities/push_notifications=false
user_data/accessible_from_files_app=false
user_data/accessible_from_itunes_sharing=false
privacy/camera_usage_description=""
privacy/camera_usage_description_localized={}
privacy/microphone_usage_description=""
privacy/microphone_usage_description_localized={}
privacy/photolibrary_usage_description=""
privacy/photolibrary_usage_description_localized={}
icons/iphone_120x120=""
icons/iphone_180x180=""
icons/ipad_76x76=""
icons/ipad_152x152=""
icons/ipad_167x167=""
icons/app_store_1024x1024=""
icons/spotlight_40x40=""
icons/spotlight_80x80=""
icons/settings_58x58=""
icons/settings_87x87=""
icons/notification_40x40=""
icons/notification_60x60=""
storyboard/use_launch_screen_storyboard=false
storyboard/image_scale_mode=0
storyboard/custom_image@2x=""
storyboard/custom_image@3x=""
storyboard/use_custom_bg_color=false
storyboard/custom_bg_color=Color(0, 0, 0, 1)
landscape_launch_screens/iphone_2436x1125=""
landscape_launch_screens/iphone_2208x1242=""
landscape_launch_screens/ipad_1024x768=""
landscape_launch_screens/ipad_2048x1536=""
portrait_launch_screens/iphone_640x960=""
portrait_launch_screens/iphone_640x1136=""
portrait_launch_screens/iphone_750x1334=""
portrait_launch_screens/iphone_1125x2436=""
portrait_launch_screens/ipad_768x1024=""
portrait_launch_screens/ipad_1536x2048=""
portrait_launch_screens/iphone_1242x2208=""
[preset.4]
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times-x64.exe"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key=""
[preset.4.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_script=1
binary_format/embed_pck=false
texture_format/bptc=false
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/identity_type=0
codesign/identity=""
codesign/password=""
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=false
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"

0
exports/.gdignore Normal file
View File

47
exports/install-linux.sh Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
usage(){
echo "Copies the rat-times binary, data, and icon to the specified directory,"
echo "then copies the desktop file to ~/.local/share/applications, while"
echo "setting the correct path"
echo ""
echo "Example Usage: install-linux.sh ~/Applications"
}
if [ $# -eq 0 ]; then
>&2 echo "No arguments provided, please provide a valid path for installation"
usage
exit 1
fi
if [ $1 = "--help" ]; then
usage
exit 0
fi
if [ $1 = "-h" ]; then
usage
exit 0
fi
dir_resolve(){
cd "$1" 2>/dev/null || return $? # cd to desired directory; if fail, quell any error messages but return exit status
echo "`pwd -P`" # output full, link-resolved path
}
if APP_PATH="`dir_resolve \"$1\"`"; then
SHORTCUT_PATH="$HOME/.local/share/applications/rat-times.desktop"
mkdir -p $APP_PATH
cp rat-times.x86_64 rat-times.pck rat-times.svg $APP_PATH
sed "s@REPLACE_WITH_PATH@$APP_PATH@g" rat-times.desktop > $SHORTCUT_PATH
echo "Rat Times is installed. To remove, please delete:"
echo "$APP_PATH/rat-times.x86_64"
echo "$APP_PATH/rat-times.pck"
echo "$APP_PATH/rat-times.svg"
echo "$SHORTCUT_PATH"
else
echo "Could not reach $1"
fi

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Name=Rat Times
Comment=Track your time
Exec=REPLACE_WITH_PATH/rat-times.x86_64
Icon=REPLACE_WITH_PATH/rat-times.svg
Terminal=false
Type=Application
Categories=Game;

87
exports/rat-times.svg Normal file
View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64"
height="64"
viewBox="0 0 16.933333 16.933333"
version="1.1"
id="svg13794"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview13796"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="6.2502245"
inkscape:cx="22.479193"
inkscape:cy="24.159132"
inkscape:window-width="1896"
inkscape:window-height="1029"
inkscape:window-x="12"
inkscape:window-y="39"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs13791" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#1d86ff;fill-opacity:1;stroke-width:1.85208;paint-order:stroke markers fill;stop-color:#000000"
id="circle14391"
cx="4.0016642"
cy="4.8131599"
rx="2.9889076"
ry="2.9889104" />
<path
style="fill:#d588ff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 10.040695,15.90349 6.892697,-1.588035 -1.494564,-0.466127 1.045969,-0.83042 -1.837551,0.445927 v -0.84077 z"
id="path14447" />
<path
style="fill:#d588ff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 6.8926369,16.389379 -5.9950227e-5,14.801343 1.4945043,14.335217 0.44853556,13.504797 2.2860868,13.950724 v -0.84077 z"
id="path14449" />
<ellipse
style="fill:#1d86ff;fill-opacity:1;stroke-width:1.85208;paint-order:stroke markers fill;stop-color:#000000"
id="path13969"
cx="8.1552019"
cy="9.9386654"
rx="6.4920444"
ry="6.4920506" />
<path
sodipodi:type="spiral"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#4200cb;stroke-width:0.230262;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path14024"
sodipodi:cx="8.4666662"
sodipodi:cy="8.4666662"
sodipodi:expansion="2"
sodipodi:revolution="1.5184656"
sodipodi:radius="2.3796637"
sodipodi:argument="-19.12742"
sodipodi:t0="0"
d="m 8.4666662,8.4666662 c 0.027537,-0.00785 0.021565,0.052556 0.017694,0.06203 -0.044361,0.108565 -0.2002796,0.066923 -0.2658124,0.00874 C 8.0210701,8.3621249 8.1306116,8.0511576 8.307424,7.9083989 8.6808232,7.6069157 9.2185608,7.8211538 9.4591415,8.1835689 9.8827565,8.8217104 9.5243913,9.6591442 8.9090057,10.017409 7.9391013,10.582067 6.7291388,10.039501 6.2335968,9.1036351 6.1901155,9.0215177 6.1515675,8.9367957 6.1180994,8.8501155"
transform="matrix(-3.9529526,2.2775757,-2.3165148,-4.0094692,60.938259,26.017524)"
inkscape:transform-center-x="-0.51754845"
inkscape:transform-center-y="-2.4484256" />
<ellipse
style="fill:#1d86ff;fill-opacity:1;stroke-width:1.85208;paint-order:stroke markers fill;stop-color:#000000"
id="circle14389"
cx="11.931787"
cy="4.8131599"
rx="2.9889076"
ry="2.9889104" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
exports/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

34
main.gd
View File

@ -1,34 +0,0 @@
#!/usr/bin/env -S godot --headless -s
extends SceneTree
var config: ConfigManager = preload("res://config_manager.tres")
func _init():
var cmd := CMD.new()
for command in ["list", "stop", "start", "current"]:
if cmd.has_argument(command):
call(command)
return
print("no command provided -- exiting")
quit()
func list() -> void:
var entries := config.timesheet.entries
for item in entries:
print(item)
quit()
func stop() -> void:
if config.timesheet.current_entry:
config.timesheet.close_entry()
func start() -> void:
pass
func current() -> void:
if config.timesheet.current_entry:
print("{name}\t{start_time}\t{end_time}"%config.timesheet.current_entry)

48
main_cli.gd Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env -S godot --no-window -s
extends SceneTree
var config: ConfigManager = preload("res://config_manager.tres")
const valid_commands := ["list", "stop", "start"]
func _init():
var cmd := CMD.new()
for command in valid_commands:
if cmd.has_argument(command):
call(command, cmd.get_argument(command))
return
print("no command provided -- exiting")
help()
quit(1)
func help(_show: bool = true) -> void:
print("Valid commands are:")
for command in valid_commands:
prints(" -",command)
func list(show_all: bool = true) -> void:
var entries := config.timesheet.entries
for item in entries:
if show_all:
print(item)
else:
if not item.is_closed:
print(item)
quit()
func stop(entry_name: String) -> void:
var success := config.timesheet.stop_entry(entry_name)
if success:
print("closed %s"%[entry_name])
else:
print("could not close %s"%[entry_name])
quit()
func start(entry_name: String) -> void:
# warning-ignore:return_value_discarded
config.timesheet.add_entry(entry_name)
quit()

View File

@ -6,39 +6,111 @@
; [section] ; section goes between [] ; [section] ; section goes between []
; param=value ; assign values to parameters ; param=value ; assign values to parameters
config_version=5 config_version=4
_global_script_classes=[ {
"base": "Reference",
"class": "CMD",
"language": "GDScript",
"path": "res://scripts/cmd.gd"
}, {
"base": "Resource",
"class": "ConfigManager",
"language": "GDScript",
"path": "res://scripts/config_manager.gd"
}, {
"base": "Reference",
"class": "Consts",
"language": "GDScript",
"path": "res://scripts/consts.gd"
}, {
"base": "Reference",
"class": "FileWatcher",
"language": "GDScript",
"path": "res://scripts/file_watcher.gd"
}, {
"base": "Tree",
"class": "TimeEntriesItemsTree",
"language": "GDScript",
"path": "res://ui/tasks_list.gd"
}, {
"base": "Reference",
"class": "TimeEntry",
"language": "GDScript",
"path": "res://scripts/time_entry.gd"
}, {
"base": "Reference",
"class": "TimeEntryTreeItem",
"language": "GDScript",
"path": "res://scripts/time_entry_tree_item.gd"
}, {
"base": "Reference",
"class": "TimeSheet",
"language": "GDScript",
"path": "res://scripts/time_sheet.gd"
}, {
"base": "Reference",
"class": "TimeStamp",
"language": "GDScript",
"path": "res://scripts/time_stamp.gd"
} ]
_global_script_class_icons={
"CMD": "",
"ConfigManager": "",
"Consts": "",
"FileWatcher": "",
"TimeEntriesItemsTree": "",
"TimeEntry": "",
"TimeEntryTreeItem": "",
"TimeSheet": "",
"TimeStamp": ""
}
[application] [application]
config/name="Rat Times" config/name="Rat Times"
config/description="Track your time(s)s" run/main_scene="res://ui/composed_ui.tscn"
run/main_scene="res://ui/Main.tscn"
config/use_custom_user_dir=true config/use_custom_user_dir=true
config/custom_user_dir_name="rat_times" config/custom_user_dir_name="rat_times"
config/features=PackedStringArray("4.0")
run/low_processor_mode=true run/low_processor_mode=true
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
boot_splash/show_image=false boot_splash/show_image=false
config/icon="res://assets/logo.svg" boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 0 )
config/macos_native_icon="res://logo.icns" config/icon="res://assets/logo.png"
config/windows_native_icon="res://logo.ico" config/macos_native_icon="res://build/logo.icns"
config/windows_native_icon="res://build/logo.ico"
[debug]
settings/fps/force_fps=60
[display] [display]
window/size/viewport_width=500 window/size/width=360
window/size/viewport_height=151 window/size/height=760
window/size/resizable=false window/size/resizable=false
window/size/always_on_top=true window/size/always_on_top=true
window/size/transparent=true
window/per_pixel_transparency/allowed=true window/per_pixel_transparency/allowed=true
window/subwindows/embed_subwindows=false window/per_pixel_transparency/enabled=true
window/energy_saving/keep_screen_on=false
window/handheld/orientation="portrait"
window/ios/hide_home_indicator=false
window/stretch/aspect="keep"
[physics] [global]
common/enable_pause_aware_picking=true rced=false
[gui]
theme/use_hidpi=true
[logging]
file_logging/log_path="user://logs/rat-times.log"
[rendering] [rendering]
textures/vram_compression/import_etc2_astc=true quality/driver/driver_name="GLES2"
viewport/transparent_background=true vram_compression/import_etc=true
vram_compression/import_etc2=false
environment/default_environment="res://default_env.tres" environment/default_environment="res://default_env.tres"

View File

@ -1,5 +1,23 @@
class_name CMD class_name CMD
var _parsed := false
## @type Dictionary[String, String|bool]
var command_line_arguments: Dictionary = {} setget set_command_line_arguments, get_command_line_arguments
## Removes the first element find from the `quotes` array from the start and end of a string
## Also removes any whitespace resulting from removing the quoting elements
static func unsurround(value: String, quotes := PoolStringArray(['"', "'"])) -> String:
for quote_str in quotes:
if value.begins_with(quote_str) \
and value.ends_with(quote_str) \
and value[value.length() - 2] != "\\":
return value.trim_prefix(quote_str).trim_suffix(quote_str).strip_edges()
return value
## Returns a dictionary of all arguments passed after `--` on the command line ## Returns a dictionary of all arguments passed after `--` on the command line
## arguments take one of 2 forms: ## arguments take one of 2 forms:
## - `--arg` which is a boolean (using `--no-arg` for `false` is possible) ## - `--arg` which is a boolean (using `--no-arg` for `false` is possible)
@ -7,21 +25,14 @@ class_name CMD
## unsurround the string ## unsurround the string
## This function does no evaluation and does not attempt to guess the type of ## This function does no evaluation and does not attempt to guess the type of
## arguments. You will receive either bools, or strings. ## arguments. You will receive either bools, or strings.
var command_line_arguments: Dictionary = (func () -> Dictionary: static func parse_cmd_arguments() -> Dictionary:
var unsurround := func unsurround(value: String, quotes := PackedStringArray(['"', "'"])) -> String:
for quote_str in quotes:
if value.begins_with(quote_str) \
and value.ends_with(quote_str) \
and value[value.length() - 2] != "\\":
return value.trim_prefix(quote_str).trim_suffix(quote_str).strip_edges()
return value
var arguments := {} var arguments := {}
for argument in OS.get_cmdline_user_args(): for arg in OS.get_cmdline_args():
argument = argument.lstrip("--").to_lower() var argument: String = arg.lstrip("--").to_lower()
if argument.find("=") > -1: if argument.find("=") > -1:
var arg_tuple := argument.split("=") var arg_tuple := argument.split("=")
var key := arg_tuple[0] var key := arg_tuple[0]
var value:String = unsurround.call(arg_tuple[1]) var value := unsurround(arg_tuple[1])
arguments[key] = value arguments[key] = value
else: else:
var key := argument var key := argument
@ -30,14 +41,30 @@ var command_line_arguments: Dictionary = (func () -> Dictionary:
value = false value = false
key = argument.lstrip("no-") key = argument.lstrip("no-")
arguments[key] = value arguments[key] = value
return arguments).call() return arguments
func get_argument(name: String, default: Variant = null) -> Variant: func set_command_line_arguments(_arguments: Dictionary) -> void:
if command_line_arguments.has(name): printerr("get_command_line_arguments is a read only value")
return command_line_arguments[name]
func get_command_line_arguments() -> Dictionary:
if not _parsed:
_parsed = true
command_line_arguments = parse_cmd_arguments()
return command_line_arguments
## Returns a single argument passed after `--` on the command line
## if the argument does not exist, `default` is returned instead
## _parse_cmd_arguments() has to be called first
func get_argument(name: String, default = null):
if get_command_line_arguments().has(name):
return get_command_line_arguments()[name]
return default return default
## Verifies an argument exists on the command line
## _parse_cmd_arguments() has to be called first
func has_argument(name: String) -> bool: func has_argument(name: String) -> bool:
return command_line_arguments.has(name) return get_command_line_arguments().has(name)

View File

@ -1,85 +1,180 @@
## Reads the config, sets values. Acts a singleton because it proxies a const
## file path.
class_name ConfigManager extends Resource class_name ConfigManager extends Resource
const CONFIG_PATH := "user://settings.cfg" const CONFIG_PATH := "user://settings.cfg"
var _config := ConfigFile.new() var _config := ConfigFile.new()
var _watcher: FileWatcher
###############################################################################
#
# SIGNAL
#
signal time_sheet_loaded
func emit_loaded() -> void:
emit_signal("time_sheet_loaded")
var timesheet: TimeSheet: ###############################################################################
get: #
if timesheet == null: # TIMESHEET FILE LOADING AND PARSING
timesheet = TimeSheet.restore(current_file) #
return timesheet
signal file_changed var timesheet: TimeSheet setget ,get_timesheet
var current_file: String = "":
set = set_current_file,
get = get_current_file
func set_current_file(value: String) -> void: func get_timesheet() -> TimeSheet:
timesheet = TimeSheet.restore(value) if timesheet == null:
timesheet = _load_timesheet(get_current_timesheet_file_path())
return timesheet
func _load_timesheet(path: String) -> TimeSheet:
var new_timesheet := TimeSheet.restore(path)
if new_timesheet == null:
return null
_watcher = FileWatcher.new()
_watcher.file_name = path
# warning-ignore:return_value_discarded
_watcher.connect("file_changed", self, "reload_timesheet")
_watcher.start()
return new_timesheet
func reload_timesheet() -> void:
var new_timesheet = _load_timesheet(get_current_timesheet_file_path())
if new_timesheet == null:
printerr("failed to load new timesheet")
return
timesheet = new_timesheet
emit_loaded()
###############################################################################
#
# TIMESHEET FILE PATH
#
var current_timesheet_file_path: String = "" setget set_current_timesheet_file_path, get_current_timesheet_file_path
func set_current_timesheet_file_path(value: String) -> void:
if current_timesheet_file_path == value:
return
timesheet = _load_timesheet(value)
if timesheet == null: if timesheet == null:
return return
current_file = value current_timesheet_file_path = value
_config.set_value("MAIN", "file", value) _config.set_value("MAIN", "file", value)
file_changed.emit() emit_loaded()
save() save()
func get_current_file() -> String: func get_current_timesheet_file_path() -> String:
var _default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv") var _default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).plus_file("mouse_timer.csv")
return _config.get_value("MAIN", "file", _default_path) return _config.get_value("MAIN", "file", _default_path)
var theme: Theme: ###############################################################################
get: #
# THEME FILE LOADING AND PARSING
#
var theme: Theme setget , get_theme
func get_theme() -> Theme:
if theme == null: if theme == null:
theme = ResourceLoader.load(theme_path, "Theme") theme = ResourceLoader.load(theme_file_path, "Theme")
return theme return theme
###############################################################################
#
# THEME FILE PATH
#
signal theme_changed signal theme_changed
var theme_path: String = "": var theme_file_path: String = "" setget set_theme_file_path, get_theme_file_path
set = set_theme_path,
get = get_theme_path
func set_theme_path(value: String) -> void: func set_theme_file_path(value: String) -> void:
var new_theme: Theme = ResourceLoader.load(value, "Theme") var new_theme: Theme = ResourceLoader.load(value, "Theme")
if new_theme != null: if new_theme != null:
theme = new_theme theme = new_theme
theme_path = value theme_file_path = value
_config.set_value("MAIN", "theme", value) _config.set_value("MAIN", "theme", value)
theme_changed.emit() emit_signal("theme_changed")
save() save()
func get_theme_path() -> String: func get_theme_file_path() -> String:
return _config.get_value("MAIN", "theme", preload("res://assets/default_theme.theme").resource_path) return _config.get_value("MAIN", "theme", preload("res://assets/default_theme.theme").resource_path)
var last_task_name: String = "": ###############################################################################
set(value): #
last_task_name = value # SOUND OPTION
_config.set_value("MAIN", "last_task_name", value) #
save()
get:
return _config.get_value("MAIN", "last_task_name", "")
var sound_fx_on: bool = true setget set_sound_fx_on, get_sound_fx_on
var sound_fx_on: bool = true: func set_sound_fx_on(value: bool) -> void:
set(value):
sound_fx_on = value sound_fx_on = value
_config.set_value("MAIN", "sound_fx_on", value) _config.set_value("MAIN", "sound_fx_on", value)
save() save()
get:
func get_sound_fx_on() -> bool:
return _config.get_value("MAIN", "sound_fx", true) return _config.get_value("MAIN", "sound_fx", true)
###############################################################################
#
# SOME SETTINGS CACHE
#
var current_task_name := "" setget set_current_task_name, get_current_task_name
func set_current_task_name(value: String) -> void:
current_task_name = value
_config.set_value("CACHE", "current_task_name", value)
save()
func get_current_task_name() -> String:
return _config.get_value("CACHE", "current_task_name", "")
var last_window_position := Vector2() setget set_last_window_position, get_last_window_position
func set_last_window_position(value: Vector2) -> void:
last_window_position = value
_config.set_value("CACHE", "last_window_position", value)
save()
func get_last_window_position() -> Vector2:
return _config.get_value("CACHE", "last_window_position", OS.get_screen_size()/2)
###############################################################################
#
# BOOTSTRAP
#
func _init() -> void: func _init() -> void:
# warning-ignore:return_value_discarded
_config.load(CONFIG_PATH) _config.load(CONFIG_PATH)
func save() -> void: func save() -> void:
# warning-ignore:return_value_discarded
_config.save(CONFIG_PATH) _config.save(CONFIG_PATH)

60
scripts/file_watcher.gd Normal file
View File

@ -0,0 +1,60 @@
class_name FileWatcher
signal file_changed
var file_name: String setget set_file_name
var scan_delay_s: float = 1.0 setget set_scan_delay_s
var _file := File.new()
var _last_modified: int = 0
var _timer := Timer.new()
func _init() -> void:
# warning-ignore:return_value_discarded
_timer.connect("timeout", self, "check")
_timer.wait_time = scan_delay_s
func set_file_name(new_file_name: String) -> void:
if file_name == new_file_name:
return
file_name = new_file_name
if file_name.begins_with("res://") or file_name.begins_with("user://"):
file_name = ProjectSettings.globalize_path(file_name)
_last_modified = _file.get_modified_time(file_name)
_timer.name = "__file_watcher_%s"%[file_name]
func set_scan_delay_s(new_scan_delay_s: float) -> void:
scan_delay_s = new_scan_delay_s
_timer.wait_time = scan_delay_s
func check() -> void:
if file_name == "":
return
var new_modified := _file.get_modified_time(file_name)
if new_modified != _last_modified:
_last_modified = new_modified
emit_signal("file_changed")
func start() -> void:
if not _timer.is_inside_tree():
var main_loop: SceneTree = Engine.get_main_loop()
var root := main_loop.root
if root == null:
printerr("Called start before any scene is loaded")
return
if not root.is_inside_tree():
yield(root, "ready")
root.call_deferred("add_child", _timer, true)
yield(_timer, "ready")
_timer.start()
func stop() -> void:
_timer.stop()

View File

@ -2,21 +2,29 @@
## Has a beginning and an end ## Has a beginning and an end
class_name TimeEntry class_name TimeEntry
var name := "" var name := ""
var closed := false var is_closed := false
var start_time := TimeStamp.new() var start_time := TimeStamp.new()
var end_time := TimeStamp.new() var end_time := TimeStamp.new()
var previous_total := 0
func start_recording() -> TimeEntry: func start_recording() -> TimeEntry:
start_time = start_time.from_current_time() # warning-ignore:return_value_discarded
end_time = end_time.from_current_time() start_time.from_current_time()
# warning-ignore:return_value_discarded
end_time.from_current_time()
return self return self
func update() -> void: func update() -> void:
end_time = end_time.from_current_time() # warning-ignore:return_value_discarded
end_time.from_current_time()
func close() -> void:
update()
is_closed = true
func get_elapsed_seconds() -> int: func get_elapsed_seconds() -> int:
@ -24,56 +32,72 @@ func get_elapsed_seconds() -> int:
return elapsed return elapsed
func get_total_elapsed_seconds() -> int:
var elapsed := get_elapsed_seconds() + previous_total
return elapsed
func get_period() -> String: func get_period() -> String:
var time_in_secs := get_elapsed_seconds() var time_in_secs := get_elapsed_seconds()
return TimeEntry.time_to_period(time_in_secs) return time_to_period(time_in_secs)
func get_total_period() -> String:
var time_in_secs := get_total_elapsed_seconds()
return TimeEntry.time_to_period(time_in_secs)
static func time_to_period(time_in_secs: int) -> String: static func time_to_period(time_in_secs: int) -> String:
# warning-ignore:integer_division
var seconds := time_in_secs%60 var seconds := time_in_secs%60
@warning_ignore("integer_division") # warning-ignore:integer_division
var minutes := (time_in_secs/60)%60 var minutes := (time_in_secs/60)%60
@warning_ignore("integer_division") # warning-ignore:integer_division
var hours := (time_in_secs/60)/60 var hours := (time_in_secs/60)/60
return "%02d:%02d:%02d" % [hours, minutes, seconds] return "%02d:%02d:%02d" % [hours, minutes, seconds]
func to_csv_line() -> PackedStringArray:
return PackedStringArray([ static func period_to_time(period_string: String) -> int:
var period := period_string.split(":")
if period.size() < 3:
return -1
var hours := int(period[0])
var minutes := int(period[1])
var seconds := int(period[2])
var time := seconds + (minutes * 60) + (hours * 60 * 60)
return time
func to_csv_line() -> PoolStringArray:
return PoolStringArray([
name, name,
start_time, start_time,
end_time, end_time,
str(get_elapsed_seconds()) if closed else tr(Consts.ONGOING) str(get_elapsed_seconds()) if is_closed else tr(Consts.ONGOING)
]) ])
static func is_csv_line_valid(line: PackedStringArray) -> bool:
static func is_csv_line_valid(line: PoolStringArray) -> bool:
return line.size() > 3 return line.size() > 3
func from_csv_line(line: PackedStringArray) -> TimeEntry: func from_csv_line(line: PoolStringArray) -> TimeEntry:
name = line[0] name = line[0]
var start_time_string = line[1] var start_time_string = line[1]
# warning-ignore:return_value_discarded
start_time.from_string(start_time_string) start_time.from_string(start_time_string)
var elapsed_seconds = int(line[3]) if line[3].is_valid_int() else 0 var elapsed_seconds = int(line[3]) if line[3].is_valid_integer() else 0
closed = elapsed_seconds > 0 is_closed = elapsed_seconds > 0
if closed == true: if is_closed == true:
var end_time_string = line[2] var end_time_string = line[2]
# warning-ignore:return_value_discarded
end_time.from_string(end_time_string) end_time.from_string(end_time_string)
else: else:
# warning-ignore:return_value_discarded
end_time.from_current_time() end_time.from_current_time()
return self return self
func to_dict() -> Dictionary:
return {
start_time = start_time,
closed = is_closed,
}
func _to_string() -> String: func _to_string() -> String:
return "%s\t%s\t%s"%[name, Consts.ONGOING if closed == false else "", start_time] return "%s\t%s\t%s"%[name, tr(Consts.ONGOING) if is_closed == false else end_time.to_string(), start_time]

View File

@ -0,0 +1,77 @@
class_name TimeEntryTreeItem
signal end_time_updated
var time_entry: TimeEntry
var children := {}
var time_entries := []
func get_child(parts: Array, or_create := false):
# workaround for cyclic dependencies bug
var TimeEntryTreeItem = load("res://scripts/time_entry_tree_item.gd")
if parts.size() == 0:
return self
var part: String = parts.pop_front()
if not children.has(part):
if or_create == false:
return null
var time_entry_tree_item = TimeEntryTreeItem.new()
# warning-ignore:return_value_discarded
time_entry_tree_item.connect("end_time_updated", self, "_on_end_time_updated")
children[part] = time_entry_tree_item
return children[part].get_child(parts, or_create)
func find_active_time_entry() -> TimeEntry:
for _time_entry_tree_item in time_entries:
var time_entry_tree_item := _time_entry_tree_item as TimeEntryTreeItem
var current_time_entry := time_entry_tree_item.time_entry
if not current_time_entry.is_closed:
return current_time_entry
return null
func append(new_time_entry: TimeEntry) -> void:
var TimeEntryTreeItem = load("res://scripts/time_entry_tree_item.gd")
var time_entry_tree_item = TimeEntryTreeItem.new()
time_entry_tree_item.time_entry = new_time_entry
time_entries.append(time_entry_tree_item)
func _on_end_time_updated() -> void:
emit_signal("end_time_updated")
func get_elapsed_seconds() -> int:
var seconds := time_entry.get_elapsed_seconds() if time_entry != null else 0
for child_name in children:
var child = children[child_name]
seconds += child.get_elapsed_seconds()
for entry in time_entries:
seconds += entry.get_elapsed_seconds()
return seconds
func get_period() -> String:
var time_in_secs := get_elapsed_seconds()
return TimeEntry.time_to_period(time_in_secs)
func to_dict() -> Dictionary:
var json := {}
var times := []
if time_entries.size() > 0:
for entry in time_entries:
times.append(entry.to_dict())
json["__time"] = times
if children.size() > 0:
for name in children:
json[name] = children[name].to_dict()
return json
func _to_string() -> String:
var json := to_dict()
var json_string := JSON.print(json, "\t")
return json_string

View File

@ -2,19 +2,21 @@ class_name TimeSheet
var source_path := "" var source_path := ""
var entries: Array[TimeEntry] = [] var entries := []
var entries_names := {} # warning-ignore:integer_division
var current_entry: TimeEntry var _last_update := Time.get_ticks_msec() / 1000
## Loads the data file ## Loads the data file
func load_file() -> bool: func load_file() -> bool:
var file := FileAccess.open(source_path, FileAccess.READ) var file := File.new()
if file == null: var success := file.open(source_path, File.READ)
file = FileAccess.open(source_path, FileAccess.WRITE) if success != OK:
if file == null: success = file.open(source_path, File.WRITE)
if success != OK:
printerr("Failed to open file %s"%[ProjectSettings.globalize_path(source_path)]) printerr("Failed to open file %s"%[ProjectSettings.globalize_path(source_path)])
return false return false
return true return true
while not file.eof_reached(): while not file.eof_reached():
var line := file.get_csv_line() var line := file.get_csv_line()
if line.size() == 0 or "".join(line).length() == 0: if line.size() == 0 or "".join(line).length() == 0:
@ -24,63 +26,145 @@ func load_file() -> bool:
continue continue
var entry := TimeEntry.new().from_csv_line(line) var entry := TimeEntry.new().from_csv_line(line)
entries.append(entry) entries.append(entry)
if entry.closed == false:
current_entry = entry
if not entries_names.has(entry.name):
entries_names[entry.name] = 0
entries_names[entry.name] += entry.get_elapsed_seconds()
file.close() file.close()
return true return true
func get_active_entry_from_name(task_name: String) -> TimeEntry:
for _entry in entries:
var current_time_entry := _entry as TimeEntry
if current_time_entry.name == task_name and not current_time_entry.is_closed:
return current_time_entry
return null
## Adds a new time entry to the tree and to the data file ## Adds a new time entry to the tree and to the data file
func start_entry(entry_name: String) -> void: func add_entry(entry_name: String) -> TimeEntry:
current_entry = TimeEntry.new().start_recording() var current_entry := TimeEntry.new().start_recording()
current_entry.name = entry_name current_entry.name = entry_name
current_entry.closed = false current_entry.is_closed = false
if entry_name in entries_names: var file := File.new()
current_entry.previous_total = entries_names[entry_name] var success := file.open(source_path, File.READ_WRITE)
var file := FileAccess.open(source_path, FileAccess.READ_WRITE) if success != OK:
if file == null:
printerr("Could not open file") printerr("Could not open file")
return return null
file.seek_end()
entries.append(current_entry) entries.append(current_entry)
file.store_csv_line(current_entry.to_csv_line()) file.store_csv_line(current_entry.to_csv_line())
return current_entry
func stop_entry(entry_name: String, do_save := true) -> bool:
for _entry in entries:
var current_time_entry := _entry as TimeEntry
if current_time_entry.name == entry_name and not current_time_entry.is_closed:
current_time_entry.close()
if do_save:
save()
return true
return false
func toggle_entry(entry_name: String, do_save := true) -> void:
if stop_entry(entry_name, do_save):
return
else:
# warning-ignore:return_value_discarded
add_entry(entry_name)
func update() -> void: func update() -> void:
current_entry.update() # warning-ignore:integer_division
entries_names[current_entry.name] = current_entry.get_total_elapsed_seconds() var current_time := Time.get_ticks_msec() / 1000
if current_time == _last_update:
return
func close_entry() -> void: _last_update = current_time
current_entry.closed = true for entry in entries:
save() var time_entry := entry as TimeEntry
if time_entry.is_closed == false:
time_entry.update()
func get_period() -> String:
return current_entry.get_period()
func get_total_elapsed_seconds() -> int:
return current_entry.get_total_elapsed_seconds()
func save() -> void: func save() -> void:
var file := FileAccess.open(source_path, FileAccess.WRITE) var file := File.new()
if file == null: var success := file.open(source_path, File.WRITE)
if success != OK:
printerr("Could not open file") printerr("Could not open file")
return return
for time_entry in entries: for time_entry in entries:
file.store_csv_line(time_entry.to_csv_line()) file.store_csv_line(time_entry.to_csv_line())
func make_items_tree() -> TimeEntryTreeItem:
var sorted_entries := EntrySorter.new(entries).sort_by([["name"], ["start_date", true]]).entries
var tree := TimeEntryTreeItem.new()
for entry_index in sorted_entries.size():
var entry := sorted_entries[entry_index] as TimeEntry
var parts := entry.name.split("/")
var repo: TimeEntryTreeItem = tree.get_child(parts, true)
repo.append(entry)
return tree
static func restore(file_path: String) -> TimeSheet: static func restore(file_path: String) -> TimeSheet:
var timesheet := TimeSheet.new() var timesheet = load("res://scripts/time_sheet.gd").new()
timesheet.source_path = file_path timesheet.source_path = file_path
var success := timesheet.load_file() var success: bool = timesheet.load_file()
if success: if success:
return timesheet return timesheet
return null return null
class EntrySorter:
var entries: Array
var _sorters := PoolStringArray()
func _init(initial_entries: Array) -> void:
entries = initial_entries.duplicate()
func by_name(reverse := false) -> EntrySorter:
return sort_by_one("name", reverse)
func by_date(reverse := false) -> EntrySorter:
return sort_by_one("date", reverse)
func sort_by_one(property: String, reverse := false) -> EntrySorter:
var method_name := "_by_%s"%[property]
assert(has_method(method_name), "%s is not a valid sorting property"%[property])
entries.sort_custom(self, method_name)
if reverse:
entries.invert()
return self
func sort_by(initial_sorters: Array) -> EntrySorter:
for item in initial_sorters:
var property = item[0]
var reversed = item[1] if item.size() > 1 else false
var method_name := "_by_%s"%[property]
assert(has_method(method_name), "%s is not a valid sorting property"%[property])
assert(reversed == null or reversed is bool, "The second item is not a boolean")
return self
_sorters = initial_sorters
entries.sort_custom(self, "__by_multiple")
_sorters = PoolStringArray()
return self
func __by_multiple(a: TimeEntry, b: TimeEntry) -> bool:
for item in _sorters:
var property = item[0]
var reversed = item[1]
var method_name := "_by_%s"%[property]
var result: bool = call(method_name, a, b)
if reversed:
result = not result
if result == false:
return false
return true
func _by_name(a: TimeEntry, b: TimeEntry) -> bool:
return a.name < b.name
func _by_date(a: TimeEntry, b: TimeEntry) -> bool:
return a.start_time < b.start_time

View File

@ -50,5 +50,24 @@ func from_string(time_string: String) -> TimeStamp:
return from_dict(time) return from_dict(time)
func from_unix_time(unix_time: int) -> TimeStamp:
var time := Time.get_datetime_dict_from_unix_time(unix_time)
return from_dict(time)
func equals(other) -> bool:
return (
other.year == year and \
other.month == month and \
other.day == day and \
other.weekday == weekday and \
other.hour == hour and \
other.minute == minute and \
other.second == second and \
other.unix == unix
)
func _to_string() -> String: func _to_string() -> String:
return Time.get_datetime_string_from_datetime_dict(to_dict(), false) return Time.get_datetime_string_from_datetime_dict(to_dict(), false)

View File

@ -1,118 +0,0 @@
extends Control
@onready var time_label: Label = %TimeLabel
@onready var start_button: Button = %StartButton
@onready var task_name_line_edit: LineEdit = %TaskNameLineEdit
@onready var time_entries_items_tree: TimeEntriesItemsTree = %TimeEntriesItemsTree
@onready var timer: Timer = %Timer
@onready var tasks_button: Button = %TasksButton
@onready var settings_button: Button = %SettingsButton
@onready var previous_tasks_window: Window = %PreviousTasksWindow
@onready var settings_window: Window = %SettingsWindow
@onready var audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
var config: ConfigManager = preload("res://config_manager.tres")
func _ready() -> void:
config.file_changed.connect(set_initial_state)
config.theme_changed.connect(
func theme_changed() -> void:
theme = config.them
)
get_tree().set_auto_accept_quit(false)
previous_tasks_window.hide()
settings_window.hide()
timer.timeout.connect(
func on_timer_timeout() -> void:
config.timesheet.update()
time_label.text = config.timesheet.get_period()
var total_elapsed: int = config.timesheet.get_total_elapsed_seconds()
time_entries_items_tree.set_time_elapsed(total_elapsed)
)
start_button.tooltip_text = tr(Consts.START)
start_button.toggle_mode = true
start_button.toggled.connect(
func start(is_on: bool) -> void:
if config.sound_fx_on:
audio_stream_player.play()
if is_on:
config.timesheet.start_entry(task_name_line_edit.text)
set_button_as_started()
else:
config.timesheet.close_entry()
set_button_as_stopped()
)
task_name_line_edit.text = config.last_task_name
task_name_line_edit.text_changed.connect(
func(new_text: String) -> void:
config.last_task_name = new_text
)
time_entries_items_tree.item_selected.connect(
func item_selected() -> void:
task_name_line_edit.text = time_entries_items_tree.get_current_text()
)
tasks_button.toggle_mode = true
tasks_button.toggled.connect(
func tasks_toggled(is_on: bool) -> void:
previous_tasks_window.visible = is_on
)
previous_tasks_window.close_requested.connect(
func close() -> void:
tasks_button.set_pressed_no_signal(false)
previous_tasks_window.hide()
)
settings_button.toggle_mode = true
settings_button.toggled.connect(
func settings_toggled(is_on: bool) -> void:
settings_window.visible = is_on
)
settings_window.close_requested.connect(
func close() -> void:
settings_button.set_pressed_no_signal(false)
settings_window.hide()
)
func set_button_as_started() -> void:
time_entries_items_tree.set_current_item(config.timesheet.current_entry.name)
start_button.tooltip_text = tr(Consts.STOP)
start_button.theme_type_variation = Consts.THEME_OVERRIDE_STOP
timer.start()
func set_button_as_stopped() -> void:
start_button.tooltip_text = tr(Consts.START)
time_label.text = Consts.NO_TIME
start_button.theme_type_variation = Consts.THEME_OVERRIDE_START
timer.stop()
func set_initial_state() -> void:
if config.timesheet.current_entry != null and config.timesheet.current_entry.closed == false:
start_button.set_pressed_no_signal(true)
set_button_as_started()
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
get_tree().quit()
## Unused; if a manual quit button is added, this would be used
func quit() -> void:
get_tree().notification(NOTIFICATION_WM_CLOSE_REQUEST)

View File

@ -1,132 +0,0 @@
[gd_scene load_steps=8 format=3 uid="uid://bmlciwscreowf"]
[ext_resource type="Theme" uid="uid://bd8ancgbfsvmd" path="res://assets/default_theme.theme" id="1_2s8h2"]
[ext_resource type="Script" path="res://ui/Main.gd" id="2_sl5q6"]
[ext_resource type="Script" path="res://ui/time_entries_items_tree.gd" id="3_oxqux"]
[ext_resource type="PackedScene" uid="uid://b07v41toqw355" path="res://ui/settings.tscn" id="4_4fa2j"]
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="4_6ajaq"]
[sub_resource type="InputEventKey" id="InputEventKey_guuii"]
device = -1
pressed = true
keycode = 32
unicode = 32
[sub_resource type="Shortcut" id="Shortcut_irhvi"]
events = [SubResource("InputEventKey_guuii")]
[node name="PanelContainer" type="PanelContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_2s8h2")
theme_type_variation = &"background"
[node name="Main" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
script = ExtResource("2_sl5q6")
[node name="VBoxContainer" type="VBoxContainer" parent="Main"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer2" type="HBoxContainer" parent="Main/VBoxContainer"]
layout_mode = 2
[node name="SettingsButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Settings"
theme_type_variation = &"settings_button"
icon_alignment = 1
[node name="TaskNameLineEdit" type="LineEdit" parent="Main/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Task Name. Use \"/\" to create subtasks"
caret_blink = true
caret_blink_interval = 0.5
[node name="TasksButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Tasks"
theme_type_variation = &"tasks_button"
icon_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Main/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="TimeLabel" type="Label" parent="Main/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
theme_type_variation = &"time_label"
text = "00:00:00"
horizontal_alignment = 1
vertical_alignment = 1
[node name="StartButton" type="Button" parent="Main/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_type_variation = &"play_button"
shortcut = SubResource("Shortcut_irhvi")
[node name="Timer" type="Timer" parent="Main"]
unique_name_in_owner = true
[node name="PreviousTasksWindow" type="Window" parent="Main"]
unique_name_in_owner = true
title = "Tasks"
size = Vector2i(300, 300)
visible = false
always_on_top = true
[node name="PanelContainer" type="PanelContainer" parent="Main/PreviousTasksWindow"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_type_variation = &"background"
[node name="MarginContainer" type="MarginContainer" parent="Main/PreviousTasksWindow/PanelContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="TimeEntriesItemsTree" type="Tree" parent="Main/PreviousTasksWindow/PanelContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
script = ExtResource("3_oxqux")
[node name="SettingsWindow" type="Window" parent="Main"]
unique_name_in_owner = true
title = "Settings"
size = Vector2i(300, 300)
visible = false
always_on_top = true
[node name="Settings" parent="Main/SettingsWindow" instance=ExtResource("4_4fa2j")]
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
stream = ExtResource("4_6ajaq")

51
ui/composed_ui.gd Normal file
View File

@ -0,0 +1,51 @@
extends Control
var config: ConfigManager = preload("res://config_manager.tres")
onready var v_box_container_primary := $"%VBoxContainerPrimary" as VBoxContainer
onready var toggle_tab_container_button := $"%ToggleTabContainerButton" as Button
onready var tab_container := $"%TabContainer" as TabContainer
func _ready() -> void:
toggle_tab_container_button.toggle_mode = true
# warning-ignore:return_value_discarded
toggle_tab_container_button.connect("toggled", self, "_on_toggle_tab_container_button_toggled")
tab_container.visible = toggle_tab_container_button.pressed
OS.window_position = config.last_window_position
set_window_size()
get_tree().get_root().set_transparent_background(true)
func _on_toggle_tab_container_button_toggled(is_toggled: bool) -> void:
tab_container.visible = is_toggled
set_window_size()
func set_window_size() -> void:
var style_box := theme.get_stylebox("panel", "background") as StyleBoxFlat
var margin := style_box.content_margin_bottom * 2
OS.window_size = v_box_container_primary.rect_size + Vector2.ONE * margin
if tab_container.visible:
OS.window_size.y += tab_container.rect_size.y
var mouse_button_is_pressed := false
var dragging_start_position = Vector2()
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.get_button_index() == 1:
mouse_button_is_pressed = !mouse_button_is_pressed
dragging_start_position = get_local_mouse_position()
func _process(_delta: float) -> void:
if mouse_button_is_pressed:
OS.set_window_position(OS.window_position + get_global_mouse_position() - dragging_start_position)
func _notification(what: int) -> void:
match what:
MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
config.last_window_position = OS.window_position
get_tree().quit()

104
ui/composed_ui.tscn Normal file
View File

@ -0,0 +1,104 @@
[gd_scene load_steps=6 format=2]
[ext_resource path="res://assets/default_theme.theme" type="Theme" id=1]
[ext_resource path="res://ui/tasks_list.tscn" type="PackedScene" id=2]
[ext_resource path="res://ui/time_counter.tscn" type="PackedScene" id=3]
[ext_resource path="res://ui/settings.tscn" type="PackedScene" id=4]
[ext_resource path="res://ui/composed_ui.gd" type="Script" id=5]
[node name="PanelContainer" type="PanelContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource( 1 )
theme_type_variation = "background"
script = ExtResource( 5 )
[node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_left = 5.0
margin_top = 5.0
margin_right = 355.0
margin_bottom = 755.0
[node name="VBoxContainerPrimary" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
margin_right = 350.0
margin_bottom = 142.0
[node name="TimeCounter" parent="VBoxContainer/VBoxContainerPrimary" instance=ExtResource( 3 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 350.0
margin_bottom = 142.0
[node name="HBoxContainer2" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter" index="0"]
margin_right = 350.0
margin_bottom = 40.0
[node name="ToggleTabContainerButton" type="Button" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter/HBoxContainer2" index="0"]
unique_name_in_owner = true
margin_right = 40.0
margin_bottom = 40.0
theme_type_variation = "panel_toggle_button"
toggle_mode = true
[node name="TaskNameLineEdit" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter/HBoxContainer2" index="1"]
margin_left = 48.0
margin_right = 350.0
margin_bottom = 40.0
[node name="HBoxContainer" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter" index="1"]
margin_top = 47.0
margin_right = 350.0
margin_bottom = 142.0
[node name="TimeLabel" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter/HBoxContainer" index="0"]
margin_top = 0.0
margin_right = 270.0
margin_bottom = 95.0
[node name="StartButton" parent="VBoxContainer/VBoxContainerPrimary/TimeCounter/HBoxContainer" index="1"]
margin_left = 278.0
margin_right = 350.0
margin_bottom = 95.0
[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
unique_name_in_owner = true
margin_top = 149.0
margin_right = 350.0
margin_bottom = 750.0
size_flags_vertical = 3
[node name="Task List" type="VBoxContainer" parent="VBoxContainer/TabContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 5.0
margin_top = 46.0
margin_right = -5.0
margin_bottom = -5.0
[node name="TasksList" parent="VBoxContainer/TabContainer/Task List" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 340.0
margin_bottom = 550.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Settings" type="ScrollContainer" parent="VBoxContainer/TabContainer"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 4.0
margin_top = 32.0
margin_right = -4.0
margin_bottom = -4.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Settings" parent="VBoxContainer/TabContainer/Settings" instance=ExtResource( 4 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 342.0
margin_bottom = 565.0
[editable path="VBoxContainer/VBoxContainerPrimary/TimeCounter"]

View File

@ -1,59 +1,124 @@
extends PanelContainer extends Control
var config: ConfigManager = preload("res://config_manager.tres") var config: ConfigManager = preload("res://config_manager.tres")
onready var file_path_line_edit: LineEdit = $"%FilePathLineEdit" as LineEdit
@onready var file_path_file_dialog: FileDialog = %FilePathFileDialog onready var file_path_button: Button = $"%FilePathButton" as Button
@onready var file_path_line_edit: LineEdit = %FilePathLineEdit onready var theme_path_button: Button = $"%ThemePathButton" as Button
@onready var file_path_button: Button = %FilePathButton onready var sound_check_box: CheckBox = $"%SoundCheckBox" as CheckBox
@onready var theme_path_file_dialog: FileDialog = %ThemePathFileDialog onready var open_data_dir_button: Button = $"%OpenDataDirButton" as Button
@onready var theme_path_button: Button = %ThemePathButton onready var file_path_open_button: Button = $"%FilePathOpenButton" as Button
@onready var sound_check_box: CheckBox = %SoundCheckBox onready var attributions_rich_text_label: RichTextLabel = $"%AttributionsRichTextLabel" as RichTextLabel
@onready var attributions_rich_text_label: RichTextLabel = %AttributionsRichTextLabel onready var file_path_file_dialog: FileDialog = $"%FilePathFileDialog" as FileDialog
@onready var open_data_dir_button: Button = %OpenDataDirButton onready var theme_path_file_dialog: FileDialog = $"%ThemePathFileDialog" as FileDialog
func _ready() -> void: func _ready() -> void:
config.theme_changed.connect( # warning-ignore:return_value_discarded
func set_current_theme() -> void: config.connect("theme_changed", self, "_on_theme_changed")
theme = config.theme
)
config.file_changed.connect( # warning-ignore:return_value_discarded
func set_current_file() -> void: config.connect("time_sheet_loaded", self, "_on_current_file_changed")
file_path_file_dialog.current_path = config.current_file
file_path_file_dialog.current_dir = config.current_file.get_base_dir()
file_path_line_edit.text = config.current_file
)
file_path_button.pressed.connect( # warning-ignore:return_value_discarded
file_path_file_dialog.popup_centered file_path_button.connect("pressed", self, "_on_file_path_button_pressed")
)
file_path_file_dialog.file_selected.connect(config.set_current_file) # warning-ignore:return_value_discarded
file_path_line_edit.text_submitted.connect(config.set_current_file) file_path_file_dialog.connect("visibility_changed", self, "_resize_on_dialog", [file_path_file_dialog])
# warning-ignore:return_value_discarded
file_path_file_dialog.connect("file_selected", self, "_on_current_file_selected")
theme_path_button.pressed.connect( # warning-ignore:return_value_discarded
theme_path_file_dialog.popup_centered file_path_line_edit.connect("text_entered", self, "_on_current_file_selected")
)
theme_path_file_dialog.file_selected.connect(config.set_theme_path) # warning-ignore:return_value_discarded
theme_path_button.connect("pressed", self, "_on_theme_path_button_pressed")
# warning-ignore:return_value_discarded
theme_path_file_dialog.connect("visibility_changed", self, "_resize_on_dialog", [theme_path_file_dialog])
# warning-ignore:return_value_discarded
theme_path_file_dialog.connect("file_selected", self, "_on_new_theme_selected")
theme_path_file_dialog.hide() theme_path_file_dialog.hide()
file_path_file_dialog.hide() file_path_file_dialog.hide()
sound_check_box.button_pressed = config.sound_fx_on sound_check_box.pressed = config.sound_fx_on
sound_check_box.toggled.connect(
func sound_toggle(is_on: bool) -> void:
config.sound_fx_on = is_on
)
attributions_rich_text_label.meta_clicked.connect(OS.shell_open)
open_data_dir_button.pressed.connect( # warning-ignore:return_value_discarded
OS.shell_open.bind(OS.get_user_data_dir()) sound_check_box.connect("toggled", self, "_on_sound_toggle")
)
# warning-ignore:return_value_discarded
attributions_rich_text_label.connect("meta_clicked", self, "_on_attributions_rich_text_label_meta_clicked")
# warning-ignore:return_value_discarded
open_data_dir_button.connect("pressed", self, "_on_open_data_dir_button_pressed")
# warning-ignore:return_value_discarded
file_path_open_button.connect("pressed", self, "_on_file_path_open_button_pressed")
_on_current_file_changed()
func _on_current_file_changed() -> void:
#file_path_file_dialog.initial_path = config.current_timesheet_file_path.get_base_dir()
file_path_line_edit.text = config.current_timesheet_file_path
func _on_theme_changed() -> void:
theme = config.theme
func _on_file_path_button_pressed() -> void:
_set_file_dialog_file_path(file_path_file_dialog, config.current_timesheet_file_path)
func _on_current_file_selected(new_file: String) -> void:
config.current_timesheet_file_path = new_file
func _on_sound_toggle(is_on: bool) -> void:
config.sound_fx_on = is_on
func _on_theme_path_button_pressed() -> void:
_set_file_dialog_file_path(theme_path_file_dialog, config.theme_file_path)
theme_path_file_dialog.show()
func _on_new_theme_selected(new_theme_path: String) -> void:
config.theme_file_path = new_theme_path
func _on_attributions_rich_text_label_meta_clicked(meta) -> void:
if meta is String:
# warning-ignore:return_value_discarded
OS.shell_open(meta)
func _on_open_data_dir_button_pressed() -> void:
_open_path(OS.get_user_data_dir())
func _on_file_path_open_button_pressed() -> void:
_open_path(config.current_timesheet_file_path.get_base_dir())
func _open_path(path: String):
var canonical_path := ProjectSettings.globalize_path(path).strip_edges()
# warning-ignore:return_value_discarded
OS.shell_open("file://"+canonical_path)
func _set_file_dialog_file_path(dialog: FileDialog, path: String) -> void:
#dialog.current_path = path
#dialog.current_file = path
dialog.current_dir = path.get_base_dir()
dialog.show()
dialog.invalidate()
var previous_window_size := OS.window_size
func _resize_on_dialog(dialog: Control) -> void:
if dialog.visible == true:
previous_window_size = OS.window_size
OS.window_size = file_path_file_dialog.rect_size
else:
OS.window_size = previous_window_size

View File

@ -1,86 +1,101 @@
[gd_scene load_steps=2 format=3 uid="uid://b07v41toqw355"] [gd_scene load_steps=2 format=2]
[ext_resource type="Script" path="res://ui/settings.gd" id="1_cmilf"] [ext_resource path="res://ui/settings.gd" type="Script" id=1]
[node name="Settings" type="PanelContainer"] [node name="Settings" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
theme_type_variation = &"background" script = ExtResource( 1 )
script = ExtResource("1_cmilf")
[node name="MarginContainer" type="MarginContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2 margin_right = 505.0
margin_bottom = 760.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2 margin_right = 505.0
margin_bottom = 45.0
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2 margin_top = 10.0
margin_right = 92.0
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"] margin_bottom = 35.0
layout_mode = 2
text = "File Path" text = "File Path"
[node name="FilePathLineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"] [node name="FilePathLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_left = 100.0
margin_right = 264.0
margin_bottom = 45.0
size_flags_horizontal = 3 size_flags_horizontal = 3
caret_blink = true caret_blink = true
caret_blink_interval = 0.5 caret_blink_speed = 0.5
[node name="FilePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"] [node name="FilePathButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_left = 272.0
margin_right = 314.0
margin_bottom = 45.0
text = "..." text = "..."
[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] [node name="FilePathOpenButton" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Alternative theme
"
[node name="ThemePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_left = 322.0
margin_right = 505.0
margin_bottom = 45.0
text = "open directory"
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
margin_top = 53.0
margin_right = 505.0
margin_bottom = 90.0
size_flags_horizontal = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer2"]
margin_top = 6.0
margin_right = 199.0
margin_bottom = 31.0
text = "Alternative Theme"
[node name="ThemePathButton" type="Button" parent="VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
margin_left = 207.0
margin_right = 278.0
margin_bottom = 37.0
text = "load" text = "load"
[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] [node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2 margin_top = 98.0
margin_right = 505.0
margin_bottom = 146.0
size_flags_horizontal = 3
[node name="SoundCheckBox" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer3"] [node name="SoundCheckBox" type="CheckBox" parent="VBoxContainer/HBoxContainer3"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_right = 134.0
button_pressed = true margin_bottom = 48.0
text = "Sounds" text = "sounds"
[node name="OpenDataDirButton" type="Button" parent="MarginContainer/VBoxContainer"] [node name="OpenDataDirButton" type="Button" parent="VBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_top = 154.0
text = "open data dir" margin_right = 505.0
margin_bottom = 191.0
text = "Open data dir"
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] [node name="AttributionsRichTextLabel" type="RichTextLabel" parent="VBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 margin_top = 199.0
margin_right = 505.0
margin_bottom = 760.0
size_flags_vertical = 3 size_flags_vertical = 3
theme_type_variation = &"small_text" theme_type_variation = "small_text"
bbcode_enabled = true bbcode_enabled = true
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino bbcode_text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
Sound: [url]https://opengameart.org/content/bubbles-pop[/url] Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
@ -94,20 +109,43 @@ This game uses Godot Engine, available under the following license:
THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
" "
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
[node name="FilePathFileDialog" type="FileDialog" parent="."] Sound: https://opengameart.org/content/bubbles-pop
unique_name_in_owner = true
title = "Pick Tracker File"
size = Vector2i(800, 600)
ok_button_text = "Save"
access = 2
filters = PackedStringArray("*.csv ; Comma Separated Files")
[node name="ThemePathFileDialog" type="FileDialog" parent="."] This game uses Godot Engine, available under the following license:
Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"
[node name="CanvasLayer" type="CanvasLayer" parent="."]
[node name="Popups" type="Control" parent="CanvasLayer"]
margin_right = 40.0
margin_bottom = 40.0
mouse_filter = 2
[node name="FilePathFileDialog" type="FileDialog" parent="CanvasLayer/Popups"]
unique_name_in_owner = true unique_name_in_owner = true
title = "Open a File" margin_right = 1092.0
size = Vector2i(800, 600) margin_bottom = 762.0
ok_button_text = "Open" window_title = "Select CSV"
file_mode = 0 dialog_hide_on_ok = true
access = 2 access = 2
filters = PackedStringArray("*.theme ; Theme Files") filters = PoolStringArray( "*.csv ; CSV Files" )
[node name="ThemePathFileDialog" type="FileDialog" parent="CanvasLayer/Popups"]
unique_name_in_owner = true
margin_right = 1092.0
margin_bottom = 762.0
window_title = "Select a Theme File"
dialog_hide_on_ok = true
mode = 0
access = 2
filters = PoolStringArray( "*.theme ; Theme Files" )

155
ui/tasks_list.gd Normal file
View File

@ -0,0 +1,155 @@
class_name TimeEntriesItemsTree extends Tree
enum COL{
TEXT,
TIME
}
const META_KEY = "time_entry"
var config: ConfigManager = preload("res://config_manager.tres")
var _timer := Timer.new()
func _ready() -> void:
hide_root = true
add_child(_timer)
# warning-ignore:return_value_discarded
connect("button_pressed", self, "_on_button_pressed")
# warning-ignore:return_value_discarded
_timer.connect("timeout", self, "_on_timer_timeout")
# warning-ignore:return_value_discarded
config.connect("time_sheet_loaded", self, "populate_entries")
# warning-ignore:return_value_discarded
connect("item_edited", self, "on_Tree_item_edited")
populate_entries()
func on_Tree_item_edited():
var tree_item := get_edited()
var time_entry := tree_item.get_meta(META_KEY) as TimeEntry
if time_entry == null:
return
var edited_start_time_str := tree_item.get_text(COL.TEXT)
var edited_start_time := TimeStamp.new().from_string(edited_start_time_str)
if edited_start_time.year == 1970:
_update_from_time_entry(time_entry, tree_item)
return
var edited_duration_str := tree_item.get_text(COL.TIME)
var edited_duration := TimeEntry.period_to_time(edited_duration_str)
if edited_duration == -1:
_update_from_time_entry(time_entry, tree_item)
return
var edited_end_time_unix := edited_start_time.unix + edited_duration
var edited_end_time := TimeStamp.new().from_unix_time(edited_end_time_unix)
if edited_start_time.equals(time_entry.start_time) \
and edited_end_time.equals(time_entry.end_time):
_update_from_time_entry(time_entry, tree_item)
return
time_entry.start_time = edited_start_time
time_entry.end_time = edited_end_time
config.timesheet.save()
func populate_entries() -> void:
clear()
var tree_items_root := create_item()
var item_entries_tree := config.timesheet.make_items_tree()
_populate_from_entry(tree_items_root, item_entries_tree)
_timer.start()
func _on_timer_timeout() -> void:
config.timesheet.update()
func _populate_from_entry(tree_item_root: TreeItem, time_entry_item_root: TimeEntryTreeItem):
var children := time_entry_item_root.children
for time_entry_name in children:
var time_entry_item: TimeEntryTreeItem = children[time_entry_name]
var item := find_or_create_item(tree_item_root, time_entry_name)
item.set_metadata(COL.TEXT, time_entry_name)
item.set_text(COL.TIME, time_entry_item.get_period())
# warning-ignore:return_value_discarded
time_entry_item.connect("end_time_updated", self, "_on_time_entry_changed_update_item", [time_entry_item, item])
_populate_from_entry(item, time_entry_item)
var has_at_least_one_running_entry := time_entry_item.find_active_time_entry() != null
var texture := preload("res://assets/stop_small.svg") \
if has_at_least_one_running_entry \
else preload("res://assets/play_small.svg")
item.add_button(COL.TIME, texture)
var entries = time_entry_item_root.time_entries
for entry_item in entries:
var time_entry_item := entry_item as TimeEntryTreeItem
var item := create_item(tree_item_root)
var time_entry := time_entry_item.time_entry
item.set_meta(META_KEY, time_entry)
item.set_metadata(COL.TEXT, time_entry.name)
item.set_editable(COL.TEXT, true)
item.set_editable(COL.TIME, true)
_update_from_time_entry(time_entry, item)
# warning-ignore:return_value_discarded
time_entry_item.connect("end_time_updated", self, "_on_time_entry_changed_update_item", [time_entry_item, item])
func _update_from_time_entry(time_entry: TimeEntry, item: TreeItem) -> void:
item.set_text(COL.TEXT, time_entry.start_time.to_string())
item.set_text(COL.TIME, time_entry.get_period())
if time_entry.is_closed == false:
if item.get_button_count(COL.TIME) < 1:
var texture := preload("res://assets/stop_small.svg")
item.add_button(COL.TIME, texture, 0)
else:
if item.get_button_count(COL.TIME) > 0:
item.erase_button(COL.TIME, 0)
func _on_time_entry_changed_update_item(time_entry_item: TimeEntryTreeItem, item: TreeItem) -> void:
item.set_text(COL.TIME, time_entry_item.get_period())
func _on_button_pressed(item: TreeItem, _column: int, _id: int) -> void:
var task_name: String = item.get_metadata(COL.TEXT)
if task_name == "":
return
config.timesheet.toggle_entry(task_name)
## Unecessary in Godot 4, can bre replaced with get_children()
static func _get_tree_item_children(item: TreeItem):
var children = []
var child = item.get_children()
if child == null:
return children
children.append(child)
child = child.get_next()
while child != null:
children.append(child)
child = child.get_next()
return children
## Finds an item in the tree by text
func find_item(root: TreeItem, item_name: String) -> TreeItem:
for child in _get_tree_item_children(root):
if child.get_text(COL.TEXT) == item_name:
return child
return null
func find_or_create_item(root: TreeItem, item_name: String) -> TreeItem:
var child := find_item(root, item_name)
if child != null:
return child
child = create_item(root)
child.set_text(COL.TEXT, item_name)
return child

9
ui/tasks_list.tscn Normal file
View File

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://ui/tasks_list.gd" type="Script" id=1]
[node name="TasksList" type="Tree"]
anchor_right = 1.0
anchor_bottom = 1.0
columns = 2
script = ExtResource( 1 )

74
ui/time_counter.gd Normal file
View File

@ -0,0 +1,74 @@
extends VBoxContainer
var config: ConfigManager = preload("res://config_manager.tres")
onready var task_name_line_edit := $"%TaskNameLineEdit" as LineEdit
onready var time_label := $"%TimeLabel" as Label
onready var start_button := $"%StartButton" as Button
onready var timer := $"%Timer" as Timer
onready var audio_stream_player := $"%AudioStreamPlayer" as AudioStreamPlayer
var current_time_entry: TimeEntry
func _ready() -> void:
# warning-ignore:return_value_discarded
timer.connect("timeout", self, "_on_timer_timeout")
start_button.hint_tooltip = tr(Consts.START)
start_button.toggle_mode = true
# warning-ignore:return_value_discarded
start_button.connect("toggled", self, "_on_start_button_toggled")
task_name_line_edit.text = config.current_task_name
# warning-ignore:return_value_discarded
task_name_line_edit.connect("text_changed", self, "_on_task_name_line_edit_text_changed")
# warning-ignore:return_value_discarded
config.connect("time_sheet_loaded", self, "_on_time_sheet_loaded")
update_timer_state()
func _on_timer_timeout() -> void:
config.timesheet.update()
time_label.text = current_time_entry.get_period()
func _on_start_button_toggled(_is_on: bool) -> void:
if config.sound_fx_on:
audio_stream_player.play()
if current_time_entry != null:
# warning-ignore:return_value_discarded
config.timesheet.stop_entry(task_name_line_edit.text)
else:
# warning-ignore:return_value_discarded
config.timesheet.add_entry(task_name_line_edit.text)
func _on_task_name_line_edit_text_changed(new_text: String) -> void:
config.current_task_name = new_text
update_timer_state()
func _on_time_sheet_loaded():
update_timer_state()
func update_timer_state() -> void:
current_time_entry = config.timesheet.get_active_entry_from_name(config.current_task_name)
if current_time_entry:
set_button_as_started()
else:
set_button_as_stopped()
func set_button_as_stopped() -> void:
start_button.set_pressed_no_signal(false)
start_button.hint_tooltip = tr(Consts.START)
time_label.text = Consts.NO_TIME
start_button.theme_type_variation = Consts.THEME_OVERRIDE_START
timer.stop()
func set_button_as_started() -> void:
start_button.set_pressed_no_signal(true)
start_button.hint_tooltip = tr(Consts.STOP)
start_button.theme_type_variation = Consts.THEME_OVERRIDE_STOP
timer.start()

51
ui/time_counter.tscn Normal file
View File

@ -0,0 +1,51 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://ui/time_counter.gd" type="Script" id=1]
[node name="TimeCounter" type="VBoxContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
margin_right = 360.0
margin_bottom = 45.0
[node name="TaskNameLineEdit" type="LineEdit" parent="HBoxContainer2"]
unique_name_in_owner = true
margin_right = 360.0
margin_bottom = 45.0
size_flags_horizontal = 3
placeholder_text = "Task Name. Use \"/\" to create subtasks"
[node name="HBoxContainer" type="HBoxContainer" parent="."]
margin_top = 53.0
margin_right = 360.0
margin_bottom = 760.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="TimeLabel" type="Label" parent="HBoxContainer"]
unique_name_in_owner = true
margin_top = 341.0
margin_right = 328.0
margin_bottom = 366.0
size_flags_horizontal = 3
size_flags_vertical = 6
theme_type_variation = "time_label"
text = "00:00:00"
align = 1
valign = 1
[node name="StartButton" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
margin_left = 336.0
margin_right = 360.0
margin_bottom = 707.0
theme_type_variation = "play_button"
[node name="Timer" type="Timer" parent="."]
unique_name_in_owner = true
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true

View File

@ -1,69 +0,0 @@
class_name TimeEntriesItemsTree extends Tree
enum COL{
TEXT,
TIME
}
var config: ConfigManager = preload("res://config_manager.tres")
var current_item: TreeItem
func _ready() -> void:
config.file_changed.connect(populate_entries)
populate_entries()
func populate_entries() -> void:
clear()
var _root := create_item()
var entries_names := config.timesheet.entries_names
for entry_name in entries_names:
append_name_to_tree(entry_name, entries_names[entry_name])
func set_time_elapsed(total_elapsed: int) -> void:
current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed))
current_item.set_metadata(COL.TIME, total_elapsed)
func set_current_item(task_name: String) -> void:
current_item = append_name_to_tree(task_name, 0)
func get_current_text() -> String:
var item := get_selected()
if not item:
return ""
var resp = item.get_metadata(COL.TEXT)
if resp is String:
return resp
return ""
## Adds a new item to the tree, or returns the old item if it exists
func append_name_to_tree(task_name: String, total_elapsed := 0) -> TreeItem:
var item := get_root()
for item_name in task_name.split("/"):
item.collapsed = false
item = find_item(item, item_name, true)
item.set_metadata(COL.TEXT, task_name)
item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed))
item.set_metadata(COL.TIME, total_elapsed)
scroll_to_item(item)
return item
## Finds an item in the tree by text
func find_item(root: TreeItem, item_name: String, or_create: bool) -> TreeItem:
for child in root.get_children():
if child.get_text(COL.TEXT) == item_name:
return child
if or_create:
var child := root.create_child()
child.set_text(COL.TEXT, item_name)
child.set_text(COL.TIME, Consts.NO_TIME)
return child
return null