Compare commits

..

No commits in common. "main" and "godot-4-deprecated" have entirely different histories.

57 changed files with 981 additions and 1963 deletions

24
.gitignore vendored
View File

@ -1,22 +1,2 @@
# .gitignore generated by Hourglass
# 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
.godot/
exports/

View File

@ -1,15 +1,14 @@
# ![Logo representing a rat's butt](assets/logo.png) Rat Times
# Rat Times
Track your time, save it to a CSV file.
Uses Godot 3.5
![screenshot of the app](info/screenshot.png)
![screenshot of the app in usage](exports/screenshot.png)
Uses Godot 4
## Features
- 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)
- Custom theming possible
- Does not depend on a timer, so you can close the app immediately after starting a task

View File

@ -0,0 +1,33 @@
[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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

View File

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

View File

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

View File

@ -1,54 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

View File

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

View File

@ -1,57 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

@ -1,53 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

View File

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

View File

@ -1,7 +0,0 @@
[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]
name="HTML5"
platform="HTML5"
name="Linux/X11"
platform="Linux/X11"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/web/index.html"
script_export_mode=1
export_path="exports/rat-times-x64.x86_64"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key=""
[preset.0.options]
custom_template/debug=""
custom_template/release=""
variant/export_type=0
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color( 0, 0, 0, 1 )
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"
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
export DISPLAY=:0
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
[preset.1]
name="Mac OSX"
platform="Mac OSX"
name="macOS"
platform="macOS"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.zip"
script_export_mode=1
export_path="exports/Rat Times.zip"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key=""
[preset.1.options]
binary_format/architecture="universal"
custom_template/debug=""
custom_template/release=""
application/name="Rat Times"
application/info="Made with Godot Engine"
application/icon="res://assets/logo.icns"
application/identifier="io.mutnt.app.rattimes"
debug/export_console_script=1
application/icon="res://logo.icns"
application/icon_interpolation=4
application/bundle_identifier="org.mutnt.io.rat-times"
application/signature=""
application/app_category="Games"
application/app_category="Developer-tools"
application/short_version="1.0"
application/version="1.0"
application/copyright=""
display/high_res=false
privacy/microphone_usage_description=""
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
application/copyright_localized={}
display/high_res=true
codesign/codesign=1
codesign/identity=""
codesign/timestamp=true
codesign/hardened_runtime=true
codesign/replace_existing_signature=true
codesign/certificate_file=""
codesign/certificate_password=""
codesign/entitlements/custom_file=""
codesign/entitlements/allow_jit_code_execution=false
codesign/entitlements/allow_unsigned_executable_memory=false
@ -98,105 +98,75 @@ codesign/entitlements/app_sandbox/files_downloads=0
codesign/entitlements/app_sandbox/files_pictures=0
codesign/entitlements/app_sandbox/files_music=0
codesign/entitlements/app_sandbox/files_movies=0
codesign/custom_options=PoolStringArray( )
notarization/enable=false
codesign/entitlements/app_sandbox/helper_executables=[]
codesign/custom_options=PackedStringArray()
notarization/notarization=0
notarization/apple_id_name=""
notarization/apple_id_password=""
notarization/apple_team_id=""
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
notarization/api_uuid=""
notarization/api_key=""
notarization/api_key_id=""
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]
name="Linux/X11"
platform="Linux/X11"
name="Android"
platform="Android"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.x86_64"
script_export_mode=1
export_path="exports/rat-times-x64.apk"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
encrypt_directory=false
script_encryption_key=""
[preset.2.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
[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
gradle_build/use_gradle_build=false
gradle_build/export_format=0
gradle_build/min_sdk=""
gradle_build/target_sdk=""
architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
@ -206,13 +176,12 @@ keystore/debug_password="android"
keystore/release=""
keystore/release_user=""
keystore/release_password=""
one_click_deploy/clear_previous_install=false
version/code=1
version/name="1.0"
package/unique_name="io.mutnt.app.$genname"
package/unique_name="org.mutnt.io.ratstimes"
package/name=""
package/signed=true
package/classify_as_game=true
package/app_category=2
package/retain_data_on_uninstall=false
package/exclude_from_recents=false
launcher_icons/main_192x192=""
@ -233,7 +202,7 @@ command_line/extra_args=""
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
permissions/custom_permissions=PoolStringArray( )
permissions/custom_permissions=PackedStringArray()
permissions/access_checkin_properties=false
permissions/access_coarse_location=false
permissions/access_fine_location=false
@ -380,3 +349,145 @@ permissions/write_sms=false
permissions/write_social_stream=false
permissions/write_sync_settings=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}'"

View File

View File

@ -1,47 +0,0 @@
#!/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

@ -1,8 +0,0 @@
[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;

View File

@ -1,87 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

BIN
info/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

34
main.gd Normal file
View File

@ -0,0 +1,34 @@
#!/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)

View File

@ -1,48 +0,0 @@
#!/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,111 +6,39 @@
; [section] ; section goes between []
; param=value ; assign values to parameters
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": ""
}
config_version=5
[application]
config/name="Rat Times"
run/main_scene="res://ui/composed_ui.tscn"
config/description="Track your time(s)s"
run/main_scene="res://ui/Main.tscn"
config/use_custom_user_dir=true
config/custom_user_dir_name="rat_times"
config/features=PackedStringArray("4.0")
run/low_processor_mode=true
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
boot_splash/show_image=false
boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 0 )
config/icon="res://assets/logo.png"
config/macos_native_icon="res://build/logo.icns"
config/windows_native_icon="res://build/logo.ico"
[debug]
settings/fps/force_fps=60
config/icon="res://assets/logo.svg"
config/macos_native_icon="res://logo.icns"
config/windows_native_icon="res://logo.ico"
[display]
window/size/width=360
window/size/height=760
window/size/viewport_width=500
window/size/viewport_height=151
window/size/resizable=false
window/size/always_on_top=true
window/size/transparent=true
window/per_pixel_transparency/allowed=true
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"
window/subwindows/embed_subwindows=false
[global]
[physics]
rced=false
[gui]
theme/use_hidpi=true
[logging]
file_logging/log_path="user://logs/rat-times.log"
common/enable_pause_aware_picking=true
[rendering]
quality/driver/driver_name="GLES2"
vram_compression/import_etc=true
vram_compression/import_etc2=false
textures/vram_compression/import_etc2_astc=true
viewport/transparent_background=true
environment/default_environment="res://default_env.tres"

View File

@ -1,23 +1,5 @@
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
## arguments take one of 2 forms:
## - `--arg` which is a boolean (using `--no-arg` for `false` is possible)
@ -25,14 +7,21 @@ static func unsurround(value: String, quotes := PoolStringArray(['"', "'"])) ->
## unsurround the string
## This function does no evaluation and does not attempt to guess the type of
## arguments. You will receive either bools, or strings.
static func parse_cmd_arguments() -> Dictionary:
var command_line_arguments: Dictionary = (func () -> 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 := {}
for arg in OS.get_cmdline_args():
var argument: String = arg.lstrip("--").to_lower()
for argument in OS.get_cmdline_user_args():
argument = argument.lstrip("--").to_lower()
if argument.find("=") > -1:
var arg_tuple := argument.split("=")
var key := arg_tuple[0]
var value := unsurround(arg_tuple[1])
var value:String = unsurround.call(arg_tuple[1])
arguments[key] = value
else:
var key := argument
@ -41,30 +30,14 @@ static func parse_cmd_arguments() -> Dictionary:
value = false
key = argument.lstrip("no-")
arguments[key] = value
return arguments
return arguments).call()
func set_command_line_arguments(_arguments: Dictionary) -> void:
printerr("get_command_line_arguments is a read only value")
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]
func get_argument(name: String, default: Variant = null) -> Variant:
if command_line_arguments.has(name):
return command_line_arguments[name]
return default
## Verifies an argument exists on the command line
## _parse_cmd_arguments() has to be called first
func has_argument(name: String) -> bool:
return get_command_line_arguments().has(name)
return command_line_arguments.has(name)

View File

@ -1,180 +1,85 @@
## Reads the config, sets values. Acts a singleton because it proxies a const
## file path.
class_name ConfigManager extends Resource
const CONFIG_PATH := "user://settings.cfg"
var _config := ConfigFile.new()
var _watcher: FileWatcher
###############################################################################
#
# SIGNAL
#
signal time_sheet_loaded
func emit_loaded() -> void:
emit_signal("time_sheet_loaded")
###############################################################################
#
# TIMESHEET FILE LOADING AND PARSING
#
var timesheet: TimeSheet setget ,get_timesheet
func get_timesheet() -> TimeSheet:
var timesheet: TimeSheet:
get:
if timesheet == null:
timesheet = _load_timesheet(get_current_timesheet_file_path())
timesheet = TimeSheet.restore(current_file)
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
signal file_changed
var current_file: String = "":
set = set_current_file,
get = get_current_file
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)
func set_current_file(value: String) -> void:
timesheet = TimeSheet.restore(value)
if timesheet == null:
return
current_timesheet_file_path = value
current_file = value
_config.set_value("MAIN", "file", value)
emit_loaded()
file_changed.emit()
save()
func get_current_timesheet_file_path() -> String:
var _default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).plus_file("mouse_timer.csv")
func get_current_file() -> String:
var _default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv")
return _config.get_value("MAIN", "file", _default_path)
###############################################################################
#
# THEME FILE LOADING AND PARSING
#
var theme: Theme setget , get_theme
func get_theme() -> Theme:
var theme: Theme:
get:
if theme == null:
theme = ResourceLoader.load(theme_file_path, "Theme")
theme = ResourceLoader.load(theme_path, "Theme")
return theme
###############################################################################
#
# THEME FILE PATH
#
signal theme_changed
var theme_file_path: String = "" setget set_theme_file_path, get_theme_file_path
var theme_path: String = "":
set = set_theme_path,
get = get_theme_path
func set_theme_file_path(value: String) -> void:
func set_theme_path(value: String) -> void:
var new_theme: Theme = ResourceLoader.load(value, "Theme")
if new_theme != null:
theme = new_theme
theme_file_path = value
theme_path = value
_config.set_value("MAIN", "theme", value)
emit_signal("theme_changed")
theme_changed.emit()
save()
func get_theme_file_path() -> String:
func get_theme_path() -> String:
return _config.get_value("MAIN", "theme", preload("res://assets/default_theme.theme").resource_path)
###############################################################################
#
# SOUND OPTION
#
var last_task_name: String = "":
set(value):
last_task_name = value
_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
func set_sound_fx_on(value: bool) -> void:
var sound_fx_on: bool = true:
set(value):
sound_fx_on = value
_config.set_value("MAIN", "sound_fx_on", value)
save()
func get_sound_fx_on() -> bool:
get:
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:
# warning-ignore:return_value_discarded
_config.load(CONFIG_PATH)
func save() -> void:
# warning-ignore:return_value_discarded
_config.save(CONFIG_PATH)

View File

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

View File

@ -1,77 +0,0 @@
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,21 +2,19 @@ class_name TimeSheet
var source_path := ""
var entries := []
# warning-ignore:integer_division
var _last_update := Time.get_ticks_msec() / 1000
var entries: Array[TimeEntry] = []
var entries_names := {}
var current_entry: TimeEntry
## Loads the data file
func load_file() -> bool:
var file := File.new()
var success := file.open(source_path, File.READ)
if success != OK:
success = file.open(source_path, File.WRITE)
if success != OK:
var file := FileAccess.open(source_path, FileAccess.READ)
if file == null:
file = FileAccess.open(source_path, FileAccess.WRITE)
if file == null:
printerr("Failed to open file %s"%[ProjectSettings.globalize_path(source_path)])
return false
return true
while not file.eof_reached():
var line := file.get_csv_line()
if line.size() == 0 or "".join(line).length() == 0:
@ -26,145 +24,63 @@ func load_file() -> bool:
continue
var entry := TimeEntry.new().from_csv_line(line)
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()
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
func add_entry(entry_name: String) -> TimeEntry:
var current_entry := TimeEntry.new().start_recording()
func start_entry(entry_name: String) -> void:
current_entry = TimeEntry.new().start_recording()
current_entry.name = entry_name
current_entry.is_closed = false
var file := File.new()
var success := file.open(source_path, File.READ_WRITE)
if success != OK:
current_entry.closed = false
if entry_name in entries_names:
current_entry.previous_total = entries_names[entry_name]
var file := FileAccess.open(source_path, FileAccess.READ_WRITE)
if file == null:
printerr("Could not open file")
return null
file.seek_end()
return
entries.append(current_entry)
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:
# warning-ignore:integer_division
var current_time := Time.get_ticks_msec() / 1000
if current_time == _last_update:
return
_last_update = current_time
for entry in entries:
var time_entry := entry as TimeEntry
if time_entry.is_closed == false:
time_entry.update()
current_entry.update()
entries_names[current_entry.name] = current_entry.get_total_elapsed_seconds()
func close_entry() -> void:
current_entry.closed = true
save()
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:
var file := File.new()
var success := file.open(source_path, File.WRITE)
if success != OK:
var file := FileAccess.open(source_path, FileAccess.WRITE)
if file == null:
printerr("Could not open file")
return
for time_entry in entries:
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:
var timesheet = load("res://scripts/time_sheet.gd").new()
var timesheet := TimeSheet.new()
timesheet.source_path = file_path
var success: bool = timesheet.load_file()
var success := timesheet.load_file()
if success:
return timesheet
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,24 +50,5 @@ func from_string(time_string: String) -> TimeStamp:
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:
return Time.get_datetime_string_from_datetime_dict(to_dict(), false)

118
ui/Main.gd Normal file
View File

@ -0,0 +1,118 @@
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)

132
ui/Main.tscn Normal file
View File

@ -0,0 +1,132 @@
[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")

View File

@ -1,51 +0,0 @@
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()

View File

@ -1,104 +0,0 @@
[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,124 +1,59 @@
extends Control
extends PanelContainer
var config: ConfigManager = preload("res://config_manager.tres")
onready var file_path_line_edit: LineEdit = $"%FilePathLineEdit" as LineEdit
onready var file_path_button: Button = $"%FilePathButton" as Button
onready var theme_path_button: Button = $"%ThemePathButton" as Button
onready var sound_check_box: CheckBox = $"%SoundCheckBox" as CheckBox
onready var open_data_dir_button: Button = $"%OpenDataDirButton" as Button
onready var file_path_open_button: Button = $"%FilePathOpenButton" as Button
onready var attributions_rich_text_label: RichTextLabel = $"%AttributionsRichTextLabel" as RichTextLabel
onready var file_path_file_dialog: FileDialog = $"%FilePathFileDialog" as FileDialog
onready var theme_path_file_dialog: FileDialog = $"%ThemePathFileDialog" as FileDialog
@onready var file_path_file_dialog: FileDialog = %FilePathFileDialog
@onready var file_path_line_edit: LineEdit = %FilePathLineEdit
@onready var file_path_button: Button = %FilePathButton
@onready var theme_path_file_dialog: FileDialog = %ThemePathFileDialog
@onready var theme_path_button: Button = %ThemePathButton
@onready var sound_check_box: CheckBox = %SoundCheckBox
@onready var attributions_rich_text_label: RichTextLabel = %AttributionsRichTextLabel
@onready var open_data_dir_button: Button = %OpenDataDirButton
func _ready() -> void:
# warning-ignore:return_value_discarded
config.connect("theme_changed", self, "_on_theme_changed")
config.theme_changed.connect(
func set_current_theme() -> void:
theme = config.theme
)
# warning-ignore:return_value_discarded
config.connect("time_sheet_loaded", self, "_on_current_file_changed")
config.file_changed.connect(
func set_current_file() -> void:
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
)
# warning-ignore:return_value_discarded
file_path_button.connect("pressed", self, "_on_file_path_button_pressed")
file_path_button.pressed.connect(
file_path_file_dialog.popup_centered
)
# warning-ignore:return_value_discarded
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")
file_path_file_dialog.file_selected.connect(config.set_current_file)
file_path_line_edit.text_submitted.connect(config.set_current_file)
# warning-ignore:return_value_discarded
file_path_line_edit.connect("text_entered", self, "_on_current_file_selected")
# 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_button.pressed.connect(
theme_path_file_dialog.popup_centered
)
theme_path_file_dialog.file_selected.connect(config.set_theme_path)
theme_path_file_dialog.hide()
file_path_file_dialog.hide()
sound_check_box.pressed = config.sound_fx_on
# warning-ignore:return_value_discarded
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:
sound_check_box.button_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(
OS.shell_open.bind(OS.get_user_data_dir())
)
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,101 +1,86 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=2 format=3 uid="uid://b07v41toqw355"]
[ext_resource path="res://ui/settings.gd" type="Script" id=1]
[ext_resource type="Script" path="res://ui/settings.gd" id="1_cmilf"]
[node name="Settings" type="MarginContainer"]
[node name="Settings" type="PanelContainer"]
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
script = ExtResource( 1 )
theme_type_variation = &"background"
script = ExtResource("1_cmilf")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
margin_right = 505.0
margin_bottom = 760.0
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 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="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 505.0
margin_bottom = 45.0
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_top = 10.0
margin_right = 92.0
margin_bottom = 35.0
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "File Path"
[node name="FilePathLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
[node name="FilePathLineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 100.0
margin_right = 264.0
margin_bottom = 45.0
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
caret_blink_speed = 0.5
caret_blink_interval = 0.5
[node name="FilePathButton" type="Button" parent="VBoxContainer/HBoxContainer"]
[node name="FilePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 272.0
margin_right = 314.0
margin_bottom = 45.0
layout_mode = 2
text = "..."
[node name="FilePathOpenButton" type="Button" parent="VBoxContainer/HBoxContainer"]
[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
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
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
layout_mode = 2
text = "load"
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
margin_top = 98.0
margin_right = 505.0
margin_bottom = 146.0
size_flags_horizontal = 3
[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="SoundCheckBox" type="CheckBox" parent="VBoxContainer/HBoxContainer3"]
[node name="SoundCheckBox" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer3"]
unique_name_in_owner = true
margin_right = 134.0
margin_bottom = 48.0
text = "sounds"
layout_mode = 2
button_pressed = true
text = "Sounds"
[node name="OpenDataDirButton" type="Button" parent="VBoxContainer"]
[node name="OpenDataDirButton" type="Button" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
margin_top = 154.0
margin_right = 505.0
margin_bottom = 191.0
text = "Open data dir"
layout_mode = 2
text = "open data dir"
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="VBoxContainer"]
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
margin_top = 199.0
margin_right = 505.0
margin_bottom = 760.0
layout_mode = 2
size_flags_vertical = 3
theme_type_variation = "small_text"
theme_type_variation = &"small_text"
bbcode_enabled = true
bbcode_text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
@ -109,43 +94,20 @@ 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.
"
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
Sound: https://opengameart.org/content/bubbles-pop
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"]
[node name="FilePathFileDialog" type="FileDialog" parent="."]
unique_name_in_owner = true
margin_right = 1092.0
margin_bottom = 762.0
window_title = "Select CSV"
dialog_hide_on_ok = true
title = "Pick Tracker File"
size = Vector2i(800, 600)
ok_button_text = "Save"
access = 2
filters = PoolStringArray( "*.csv ; CSV Files" )
filters = PackedStringArray("*.csv ; Comma Separated Files")
[node name="ThemePathFileDialog" type="FileDialog" parent="CanvasLayer/Popups"]
[node name="ThemePathFileDialog" type="FileDialog" parent="."]
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
title = "Open a File"
size = Vector2i(800, 600)
ok_button_text = "Open"
file_mode = 0
access = 2
filters = PoolStringArray( "*.theme ; Theme Files" )
filters = PackedStringArray("*.theme ; Theme Files")

View File

@ -1,155 +0,0 @@
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

View File

@ -1,9 +0,0 @@
[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 )

View File

@ -1,74 +0,0 @@
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()

View File

@ -1,51 +0,0 @@
[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

@ -0,0 +1,69 @@
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