Compare commits

1 Commits

Author SHA1 Message Date
ac9e94c9b0 api for plugins 2023-03-03 21:23:11 +04:00
27 changed files with 874 additions and 1001 deletions

365
Main.gd Normal file
View File

@ -0,0 +1,365 @@
extends Control
const CONFIG_PATH := "user://settings.cfg"
enum COL{
TEXT,
TIME
}
const STRINGS := {
START = "start",
STOP = "stop",
NO_TIME = "00:00:00",
}
@onready var time_label: Label = %TimeLabel
@onready var start_button: Button = %StartButton
@onready var task_name_line_edit: LineEdit = %TaskNameLineEdit
@onready var previous_tasks_tree: Tree = %PreviousTasksTree
@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 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 audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
@onready var open_data_dir_button: Button = %OpenDataDirButton
var previous_entries: Array[TimeEntry] = []
var current_entry := TimeEntry.new()
var current_item: TreeItem
var config := ConfigFile.new()
var current_file := ""
func _init() -> void:
config.load(CONFIG_PATH)
var default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv")
current_file = config.get_value("MAIN", "file", default_path)
if config.has_section_key("MAIN", "theme"):
var new_theme: Theme = ResourceLoader.load(config.get_value("MAIN", "theme", "res://default_theme.theme"), "Theme")
if new_theme != null:
theme = new_theme
func _ready() -> void:
get_tree().set_auto_accept_quit(false)
var _root := previous_tasks_tree.create_item()
file_path_button.pressed.connect(
file_path_file_dialog.popup_centered
)
file_path_file_dialog.file_selected.connect(set_current_file)
file_path_line_edit.text_submitted.connect(set_current_file)
theme_path_button.pressed.connect(
theme_path_file_dialog.popup_centered
)
theme_path_file_dialog.file_selected.connect(
func theme_selected(theme_path: String) -> void:
var new_theme: Theme = ResourceLoader.load(theme_path, "Theme")
if new_theme != null:
theme = new_theme
config.set_value("MAIN", "theme", theme_path)
config.save(CONFIG_PATH)
)
theme_path_file_dialog.hide()
file_path_file_dialog.hide()
previous_tasks_window.hide()
settings_window.hide()
timer.timeout.connect(
func on_timer_timeout() -> void:
current_entry.update()
time_label.text = current_entry.get_period()
var total_elapsed: int = current_entry.get_total_elapsed_seconds()
current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed))
current_item.set_metadata(COL.TIME, total_elapsed)
)
start_button.tooltip_text = tr(STRINGS.START)
start_button.toggle_mode = true
start_button.toggled.connect(
func start(is_on: bool) -> void:
if sound_check_box.button_pressed:
audio_stream_player.play()
if is_on:
current_entry = TimeEntry.new().start_recording()
current_entry.name = task_name_line_edit.text
current_item = append_name_to_tree(task_name_line_edit.text)
current_entry.previous_total = current_item.get_metadata(COL.TIME)
start_button.tooltip_text = tr(STRINGS.STOP)
timer.start()
else:
add_to_entries()
start_button.tooltip_text = tr(STRINGS.START)
time_label.text = STRINGS.NO_TIME
timer.stop()
)
task_name_line_edit.text = config.get_value("MAIN", "last_task_name", "")
task_name_line_edit.text_changed.connect(
func(new_text: String) -> void:
config.set_value("MAIN", "last_task_name", new_text)
config.save(CONFIG_PATH)
)
previous_tasks_tree.item_selected.connect(
func item_selected() -> void:
var item := previous_tasks_tree.get_selected()
task_name_line_edit.text = item.get_metadata(COL.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()
)
sound_check_box.button_pressed = config.get_value("MAIN", "sound_fx", true)
sound_check_box.toggled.connect(
func sound_toggle(is_on: bool) -> void:
config.set_value("MAIN", "sound_fx", is_on)
)
open_data_dir_button.pressed.connect(
OS.shell_open.bind(OS.get_user_data_dir())
)
attributions_rich_text_label.meta_clicked.connect(OS.shell_open)
set_current_file(current_file)
## Adds a new time entry to the tree and to the data file
func add_to_entries() -> void:
previous_entries.append(current_entry)
var file := FileAccess.open(current_file, FileAccess.WRITE)
if file == null:
printerr("Could not open file")
file.store_csv_line(current_entry.to_csv_line())
## Adds a new item to the tree, or returns the old item if it exists
func append_name_to_tree(task_name: String, total_time := 0) -> TreeItem:
var item := previous_tasks_tree.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)
if not item.get_metadata(COL.TIME):
item.set_metadata(COL.TIME, total_time)
previous_tasks_tree.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, STRINGS.NO_TIME)
return child
return null
## Changes the data file path, and loads the new path
func set_current_file(new_current_file: String) -> void:
if not load_file(new_current_file):
return
previous_entries.clear()
current_file = new_current_file
config.set_value("MAIN", "file", current_file)
config.save(CONFIG_PATH)
file_path_file_dialog.current_path = current_file
file_path_file_dialog.current_dir = current_file.get_base_dir()
file_path_line_edit.text = current_file
## Loads the data file
func load_file(source_path: String) -> bool:
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
var collector := {}
while not file.eof_reached():
var line := file.get_csv_line()
if line.size() == 0 or "".join(line).length() == 0:
continue
if not TimeEntry.is_csv_line_valid(line):
push_warning("CSV Line `%s` is not conform"%[",".join(line)])
continue
var entry := TimeEntry.new().from_csv_line(line)
previous_entries.append(entry)
if not collector.has(entry.name):
collector[entry.name] = 0
collector[entry.name] += entry.get_elapsed_seconds()
for entry_name in collector:
append_name_to_tree(entry_name, collector[entry_name])
file.close()
return true
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
if start_button.button_pressed:
add_to_entries()
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)
class TimeEntry:
var name := ""
var start_time := TimeStamp.new()
var end_time := TimeStamp.new()
var previous_total := 0
func start_recording() -> TimeEntry:
start_time = start_time.from_current_time()
end_time = end_time.from_current_time()
return self
func update() -> void:
end_time = end_time.from_current_time()
func get_elapsed_seconds() -> int:
var elapsed := end_time.get_difference(start_time)
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 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:
var seconds := time_in_secs%60
@warning_ignore("integer_division")
var minutes := (time_in_secs/60)%60
@warning_ignore("integer_division")
var hours := (time_in_secs/60)/60
return "%02d:%02d:%02d" % [hours, minutes, seconds]
func to_csv_line() -> PackedStringArray:
return PackedStringArray([
name,
start_time,
end_time,
str(get_elapsed_seconds()),
])
static func is_csv_line_valid(line: PackedStringArray) -> bool:
return line.size() > 2
func from_csv_line(line: PackedStringArray) -> TimeEntry:
name = line[0]
start_time.from_string(line[1])
end_time.from_string(line[2])
return self
class TimeStamp:
var year := 0
var month := 0
var day := 0
var weekday := 0
var hour := 0
var minute := 0
var second := 0
var unix := 0
func from_current_time() -> TimeStamp:
return from_dict(Time.get_datetime_dict_from_system())
func from_dict(time: Dictionary) -> TimeStamp:
year = time.year
month = time.month
day = time.day
weekday = time.weekday
hour = time.hour
minute = time.minute
second = time.second
unix = Time.get_unix_time_from_datetime_dict(time)
return self
func to_dict() -> Dictionary:
return {
year = year,
month = month,
day = day,
weekday = weekday,
hour = hour,
minute = minute,
second = second,
}
func get_difference(other_timestamp: TimeStamp) -> int:
return unix - other_timestamp.unix
func from_string(time_string: String) -> TimeStamp:
var time := Time.get_datetime_dict_from_datetime_string(time_string, true)
return from_dict(time)
func _to_string() -> String:
return Time.get_datetime_string_from_datetime_dict(to_dict(), false)

236
Main.tscn Normal file
View File

@ -0,0 +1,236 @@
[gd_scene load_steps=6 format=3 uid="uid://bmlciwscreowf"]
[ext_resource type="Theme" uid="uid://bd8ancgbfsvmd" path="res://assets/default_theme.theme" id="1_1mila"]
[ext_resource type="Script" path="res://Main.gd" id="1_vrowr"]
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="3_o37py"]
[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_1mila")
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("1_vrowr")
[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="FilePathFileDialog" type="FileDialog" parent="Main"]
unique_name_in_owner = true
title = "Pick Tracker File"
size = Vector2i(800, 600)
ok_button_text = "Save"
access = 2
filters = PackedStringArray("*.csv ; Comma Separated Files")
[node name="ThemePathFileDialog" type="FileDialog" parent="Main"]
unique_name_in_owner = true
title = "Open a File"
size = Vector2i(800, 600)
ok_button_text = "Open"
file_mode = 0
access = 2
filters = PackedStringArray("*.theme ; Theme Files")
[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="PreviousTasksTree" 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
[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="PanelContainer" type="PanelContainer" parent="Main/SettingsWindow"]
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/SettingsWindow/PanelContainer"]
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="VBoxContainer" type="VBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[node name="HBoxContainer" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "File Path"
[node name="FilePathLineEdit" type="LineEdit" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
caret_blink_interval = 0.5
[node name="FilePathButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "..."
[node name="HBoxContainer2" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Alternative theme
"
[node name="ThemePathButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
text = "load"
[node name="HBoxContainer3" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="SoundCheckBox" type="CheckBox" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer3"]
unique_name_in_owner = true
layout_mode = 2
button_pressed = true
text = "Sounds"
[node name="OpenDataDirButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "open data dir"
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_type_variation = &"small_text"
bbcode_enabled = true
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
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="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
stream = ExtResource("3_o37py")

View File

@ -4,18 +4,6 @@ Track your time, save it to a CSV file.
![screenshot of the app](info/screenshot.png)
Uses Godot 4
That's it I guess
## Features
- Saves to custom CSV file. Sync this file with syncthing/nextcloud/dropbox/whatever you like
- 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
- Create nested tasks with `task/subtask` notation
## Coming Up
- Command line version supplied for your scripting needs or for terminal lovers
- Plugin interface to change/add functionality
- price per task
Uses Godot 4

Binary file not shown.

View File

@ -1,57 +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="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="stop.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="8.8391522"
inkscape:cx="39.766257"
inkscape:cy="42.424883"
inkscape:window-width="1896"
inkscape:window-height="977"
inkscape:window-x="12"
inkscape:window-y="91"
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="11.641667"
height="11.641667"
x="2.6458325"
y="2.6458325" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,37 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c56y8w3k75rxc"
path="res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/stop.svg"
dest_files=["res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
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/normal_map_invert_y=false
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,6 +0,0 @@
[gd_resource type="Resource" script_class="ConfigManager" load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/config_manager.gd" id="1_xfu8y"]
[resource]
script = ExtResource("1_xfu8y")

View File

@ -25,6 +25,7 @@ texture_format/bptc=false
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
texture_format/no_bptc_fallbacks=true
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@ -49,7 +50,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/Rat Times.zip"
export_path="exports/Rat Times.app"
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
@ -66,7 +67,7 @@ application/icon="res://logo.icns"
application/icon_interpolation=4
application/bundle_identifier="org.mutnt.io.rat-times"
application/signature=""
application/app_category="Developer-tools"
application/app_category="Productivity"
application/short_version="1.0"
application/version="1.0"
application/copyright=""
@ -360,7 +361,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="exports/rat-times.ipa"
export_path=""
encryption_include_filters=""
encryption_exclude_filters=""
encrypt_pck=false
@ -453,6 +454,7 @@ texture_format/bptc=false
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
texture_format/no_bptc_fallbacks=true
binary_format/architecture="x86_64"
codesign/enable=false
codesign/identity_type=0

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cpmyyivxx0dlt"
path="res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"
path="res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/logo.png"
dest_files=["res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"]
source_file="res://logo.png"
dest_files=["res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"]
[params]

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://fk5s6m8qlsei"
path="res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"
path="res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/logo.svg"
dest_files=["res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"]
source_file="res://logo.svg"
dest_files=["res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"]
[params]

34
main.gd
View File

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

249
plugin_loader.gd Normal file
View File

@ -0,0 +1,249 @@
## This resource works in two stages:
## 1. loads resource packs from a specified location
## 2. verifies each resource pack is conform to a spec
## 3. list all plugins
## 4. loads all plugins marked as "enabled"
##
## Notes: Enabling a plugin does _not_ run it. It's expected that the app would
## be restarted for the loading to happen. If you want to immediately load and
## unload, use the signals
##
## ## Expected Flow:
##
## 1. use `PluginLoader.restore().load_plugins()` to load all plugins
## 2. use `PluginLoader.restore().provide_interface()` to get a bunch of
## checkboxes to use in an UI
## 3. that's it! You can also provide an initial set of plugins (expected to be
## found with the app's files
class_name PluginLoader extends Resource
################################################################################
#
# GENERAL SINGLETON BEHAVIOR
#
## Default resource path.
const _RESOURCE_PATH := "user://plugins.tres"
## Loads the plugin loader resource.
## Always returns a PluginLoader instance; if the path is wrong, a new instance will
## be created.
static func restore(path := _RESOURCE_PATH) -> PluginLoader:
var file: PluginLoader = ResourceLoader.load(path, "PluginLoader")
if file == null:
return PluginLoader.new()
file.refresh_from_disk()
return file
## Saves the current plugins status.
## This is called automatically when changing a plugin status. You shouldn't have to
## call it manually.
func save() -> void:
ResourceSaver.save(self, _RESOURCE_PATH)
################################################################################
#
# PLUGIN PACKS LOADING
#
enum STATUS{
AVAILABLE,
LOADED,
ERROR,
NOT_FOUND
}
## Defines where plugin packs should be fetched from
@export var plugins_packs_directory := "user://"
## An object of [plugin_name: String, status: STATUS]
@export var plugins_status := {}
## These represent plugins that come with the application
@export var native_plugins := {}
## Lists all plugin packs from their designated location. Refreshes the "plugins
## status" array
## Returns a Dictionary with keys "added" and "removed", each respectively listing
## all added or removed packs as strings
func read_available_plugins() -> Dictionary:
var dir := DirAccess.open(plugins_packs_directory)
if dir == null:
return {}
dir.list_dir_begin()
var file_name = dir.get_next()
var old_plugins := plugins_status.duplicate()
plugins_status = {}
var removed: Array[String] = []
var added: Array[String] = []
# read the resource packs directory
while file_name != "":
if not dir.current_is_dir():
if file_name.get_extension().to_lower() == "pck":
var plugin_name := file_name.get_basename()
var path := plugins_packs_directory.path_join(file_name)
var success := ProjectSettings.load_resource_pack(path, false)
if not success:
printerr("Could not load resource pack %s"%[file_name])
else:
if old_plugins.has(plugin_name):
plugins_status[plugin_name] = old_plugins[plugin_name]
else:
added.append(plugin_name)
plugins_status[plugin_name] = STATUS.AVAILABLE
file_name = dir.get_next()
# read the native plugins list
for plugin_name in native_plugins:
if not old_plugins.has(plugin_name):
plugins_status[plugin_name] = old_plugins[plugin_name]
else:
plugins_status[plugin_name] = native_plugins[plugin_name]
# check if anything was removed
for plugin_name in old_plugins:
if not plugins_status.has(plugin_name):
removed.append(plugin_name)
save()
return {
added = added,
removed = removed,
}
################################################################################
#
# PLUGINS STATES
#
# Changing a plugin state does not mean anything before calling `load_plugins()`
#
signal plugin_status_changed(plugin_name: String, status: STATUS)
## Sets a plugin on or off. Does not immediately load it.
func set_plugin_status(plugin_name: String, status: STATUS) -> void:
if not plugins_status.has(plugin_name):
printerr("Plugin %s does not exist"%[plugin_name])
return
if plugins_status[plugin_name] == status:
return
plugins_status[plugin_name] = status
plugin_status_changed.emit(plugin_name, status)
save()
## Returns a plugin status.
func get_plugin_status(plugin_name: String) -> STATUS:
if not plugins_status.has(plugin_name):
printerr("Plugin %s does not exist"%[plugin_name])
return STATUS.NOT_FOUND
return (plugins_status[plugin_name]) as STATUS
################################################################################
#
# PLUGINS INSTANCING
#
## Defines where plugins are expected to be found after a plugin pack is loaded
## this is also where native plugins are located
@export var plugins_local_directory := "res://mods"
signal plugin_error(plugin_name: String)
signal plugin_started(plugin_name: String)
func start_plugin_if_enabled(plugin_name: String) -> bool:
var status := get_plugin_status(plugin_name)
match status:
STATUS.LOADED:
var instance := instance_plugin_if_valid(plugin_name)
if instance == null:
plugins_status[plugin_name] = STATUS.ERROR
plugin_error.emit(plugin_name)
else:
instance.plugin_start()
plugin_started.emit(plugin_name)
return true
return false
## This loads a local plugin and instances it. If the plugin comes from a resource pack,
## the resource pack needs to have been loaded before
func instance_plugin_if_valid(plugin_name: String) -> Plugin_V1:
var entry_point_path := plugins_local_directory.path_join(plugin_name).path_join("plugin.gd")
var entry_point_file := FileAccess.open(entry_point_path, FileAccess.READ)
if entry_point_file == null:
printerr("The entry point does not exist or is not readable")
return null
var entry_point_text := entry_point_file.get_as_text()
if not entry_point_text.match("extends"):
printerr("The entry point is not text, which is not allowed for security reasons")
return null
if entry_point_text.match("func _init():"):
printerr("The entry point contains _init(), which is a security issue, mod will not load")
return null
var entry_point_script = load(entry_point_path)
if entry_point_script == null:
printerr("Tried loading %s but did not find the expected entry point"%[entry_point_path])
return null
var instance: Plugin_V1 = (entry_point_script as Plugin_V1).new()
if not (instance is Plugin_V1):
printerr("Tried loading %s but it is not the right format"%[entry_point_path])
return null
return instance
################################################################################
#
# UTILITIES
#
## Utility function to provide a list of checkboxes that turn plugins on or off.
func provide_interface() -> Array[CheckBox]:
var checkboxes: Array[CheckBox] = []
for plugin in plugins_status:
var plugin_name: String = plugin as String
var checkbox := CheckBox.new()
checkbox.text = plugin_name.get_basename()
checkbox.button_pressed = get_plugin_status(plugin_name) == STATUS.LOADED
checkbox.toggled.connect(
func checkbox_toggled(is_on: bool) -> void:
set_plugin_status(plugin_name, STATUS.LOADED if is_on else STATUS.AVAILABLE)
)
checkboxes.append(checkbox)
return checkboxes
################################################################################
#
# BOOSTRAPPING
#
## For each plugin that's on, load it.
## This function makes no assumptions and no checks. Not loading plugins twice
## is on you
func load_plugins() -> void:
var report := read_available_plugins()
var removed := report.removed as Array[String]
var added := report.added as Array[String]
if removed.size():
print_rich("Removed plugins %s"%[", ".join(removed)])
if added.size():
print_rich("Added plugins %s"%[", ".join(removed)])
for plugin_name in plugins_status:
start_plugin_if_enabled(plugin_name)

9
plugin_v1.gd Normal file
View File

@ -0,0 +1,9 @@
class_name Plugin_V1
func plugin_start() -> void:
pass
func plugin_stop() -> void:
pass

View File

@ -12,14 +12,13 @@ config_version=5
config/name="Rat Times"
config/description="Track your time(s)s"
run/main_scene="res://ui/Main.tscn"
run/main_scene="res://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
config/icon="res://assets/logo.svg"
config/icon="res://logo.svg"
config/macos_native_icon="res://logo.icns"
config/windows_native_icon="res://logo.ico"

View File

@ -1,43 +0,0 @@
class_name CMD
## 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)
## - `--arg=value`. If the value is quoted with `"` or `'`, this function will
## 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.
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 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:String = unsurround.call(arg_tuple[1])
arguments[key] = value
else:
var key := argument
var value := true
if argument.begins_with("no-"):
value = false
key = argument.lstrip("no-")
arguments[key] = value
return arguments).call()
func get_argument(name: String, default: Variant = null) -> Variant:
if command_line_arguments.has(name):
return command_line_arguments[name]
return default
func has_argument(name: String) -> bool:
return command_line_arguments.has(name)

View File

@ -1,85 +0,0 @@
class_name ConfigManager extends Resource
const CONFIG_PATH := "user://settings.cfg"
var _config := ConfigFile.new()
var timesheet: TimeSheet:
get:
if timesheet == null:
timesheet = TimeSheet.restore(current_file)
return timesheet
signal file_changed
var current_file: String = "":
set = set_current_file,
get = get_current_file
func set_current_file(value: String) -> void:
timesheet = TimeSheet.restore(value)
if timesheet == null:
return
current_file = value
_config.set_value("MAIN", "file", value)
file_changed.emit()
save()
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)
var theme: Theme:
get:
if theme == null:
theme = ResourceLoader.load(theme_path, "Theme")
return theme
signal theme_changed
var theme_path: String = "":
set = set_theme_path,
get = get_theme_path
func set_theme_path(value: String) -> void:
var new_theme: Theme = ResourceLoader.load(value, "Theme")
if new_theme != null:
theme = new_theme
theme_path = value
_config.set_value("MAIN", "theme", value)
theme_changed.emit()
save()
func get_theme_path() -> String:
return _config.get_value("MAIN", "theme", preload("res://assets/default_theme.theme").resource_path)
var last_task_name: String = "":
set(value):
last_task_name = value
_config.set_value("MAIN", "last_task_name", value)
save()
get:
return _config.get_value("MAIN", "last_task_name", "")
var sound_fx_on: bool = true:
set(value):
sound_fx_on = value
_config.set_value("MAIN", "sound_fx_on", value)
save()
get:
return _config.get_value("MAIN", "sound_fx", true)
func _init() -> void:
_config.load(CONFIG_PATH)
func save() -> void:
_config.save(CONFIG_PATH)

View File

@ -1,8 +0,0 @@
class_name Consts
const START := "start"
const STOP := "stop"
const NO_TIME := "00:00:00"
const ONGOING := "ongoing"
const THEME_OVERRIDE_START := "play_button"
const THEME_OVERRIDE_STOP := "stop_button"

View File

@ -1,79 +0,0 @@
## Describes a row in a timesheet
## Has a beginning and an end
class_name TimeEntry
var name := ""
var closed := false
var start_time := TimeStamp.new()
var end_time := TimeStamp.new()
var previous_total := 0
func start_recording() -> TimeEntry:
start_time = start_time.from_current_time()
end_time = end_time.from_current_time()
return self
func update() -> void:
end_time = end_time.from_current_time()
func get_elapsed_seconds() -> int:
var elapsed := end_time.get_difference(start_time)
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 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:
var seconds := time_in_secs%60
@warning_ignore("integer_division")
var minutes := (time_in_secs/60)%60
@warning_ignore("integer_division")
var hours := (time_in_secs/60)/60
return "%02d:%02d:%02d" % [hours, minutes, seconds]
func to_csv_line() -> PackedStringArray:
return PackedStringArray([
name,
start_time,
end_time,
str(get_elapsed_seconds()) if closed else tr(Consts.ONGOING)
])
static func is_csv_line_valid(line: PackedStringArray) -> bool:
return line.size() > 3
func from_csv_line(line: PackedStringArray) -> TimeEntry:
name = line[0]
var start_time_string = line[1]
start_time.from_string(start_time_string)
var elapsed_seconds = int(line[3]) if line[3].is_valid_int() else 0
closed = elapsed_seconds > 0
if closed == true:
var end_time_string = line[2]
end_time.from_string(end_time_string)
else:
end_time.from_current_time()
return self
func _to_string() -> String:
return "%s\t%s\t%s"%[name, Consts.ONGOING if closed == false else "", start_time]

View File

@ -1,84 +0,0 @@
class_name TimeSheet
var source_path := ""
var entries: Array[TimeEntry] = []
var entries_names := {}
var current_entry: TimeEntry
## Loads the data file
func load_file() -> bool:
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:
continue
if not TimeEntry.is_csv_line_valid(line):
push_warning("CSV Line `%s` is not conform"%[",".join(line)])
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
## Adds a new time entry to the tree and to the data file
func start_entry(entry_name: String) -> void:
current_entry = TimeEntry.new().start_recording()
current_entry.name = entry_name
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")
entries.append(current_entry)
file.store_csv_line(current_entry.to_csv_line())
func update() -> void:
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 := FileAccess.open(source_path, FileAccess.WRITE)
if file == null:
printerr("Could not open file")
for time_entry in entries:
file.store_csv_line(time_entry.to_csv_line())
static func restore(file_path: String) -> TimeSheet:
var timesheet := TimeSheet.new()
timesheet.source_path = file_path
var success := timesheet.load_file()
if success:
return timesheet
return null

View File

@ -1,54 +0,0 @@
## A simple proxy for the object returned by Godot's time functions
## Ensures proper typing
class_name TimeStamp
var year := 0
var month := 0
var day := 0
var weekday := 0
var hour := 0
var minute := 0
var second := 0
var unix := 0
func from_current_time() -> TimeStamp:
return from_dict(Time.get_datetime_dict_from_system())
func from_dict(time: Dictionary) -> TimeStamp:
year = time.year
month = time.month
day = time.day
weekday = time.weekday
hour = time.hour
minute = time.minute
second = time.second
unix = Time.get_unix_time_from_datetime_dict(time)
return self
func to_dict() -> Dictionary:
return {
year = year,
month = month,
day = day,
weekday = weekday,
hour = hour,
minute = minute,
second = second,
}
func get_difference(other_timestamp: TimeStamp) -> int:
return unix - other_timestamp.unix
func from_string(time_string: String) -> TimeStamp:
var time := Time.get_datetime_dict_from_datetime_string(time_string, true)
return from_dict(time)
func _to_string() -> String:
return Time.get_datetime_string_from_datetime_dict(to_dict(), false)

View File

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

View File

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

View File

@ -1,59 +0,0 @@
extends PanelContainer
var config: ConfigManager = preload("res://config_manager.tres")
@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:
config.theme_changed.connect(
func set_current_theme() -> void:
theme = config.theme
)
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
)
file_path_button.pressed.connect(
file_path_file_dialog.popup_centered
)
file_path_file_dialog.file_selected.connect(config.set_current_file)
file_path_line_edit.text_submitted.connect(config.set_current_file)
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.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())
)

View File

@ -1,113 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://b07v41toqw355"]
[ext_resource type="Script" path="res://ui/settings.gd" id="1_cmilf"]
[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
theme_type_variation = &"background"
script = ExtResource("1_cmilf")
[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="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 10
[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="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
caret_blink_interval = 0.5
[node name="FilePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "..."
[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
layout_mode = 2
text = "load"
[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="SoundCheckBox" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer3"]
unique_name_in_owner = true
layout_mode = 2
button_pressed = true
text = "Sounds"
[node name="OpenDataDirButton" type="Button" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "open data dir"
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_type_variation = &"small_text"
bbcode_enabled = true
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
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="FilePathFileDialog" type="FileDialog" parent="."]
unique_name_in_owner = true
title = "Pick Tracker File"
size = Vector2i(800, 600)
ok_button_text = "Save"
access = 2
filters = PackedStringArray("*.csv ; Comma Separated Files")
[node name="ThemePathFileDialog" type="FileDialog" parent="."]
unique_name_in_owner = true
title = "Open a File"
size = Vector2i(800, 600)
ok_button_text = "Open"
file_mode = 0
access = 2
filters = PackedStringArray("*.theme ; Theme Files")

View File

@ -1,66 +0,0 @@
class_name TimeEntriesItemsTree extends Tree
enum COL{
TEXT,
TIME
}
var config: ConfigManager = preload("res://config_manager.tres")
var current_item: TreeItem
func _ready() -> void:
config.file_changed.connect(populate_entries)
populate_entries()
func populate_entries() -> void:
clear()
var _root := create_item()
var entries_names := config.timesheet.entries_names
for entry_name in entries_names:
append_name_to_tree(entry_name, entries_names[entry_name])
func set_time_elapsed(total_elapsed: int) -> void:
current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed))
current_item.set_metadata(COL.TIME, total_elapsed)
func set_current_item(task_name: String) -> void:
current_item = append_name_to_tree(task_name, 0)
func get_current_text() -> String:
var item := get_selected()
if not item:
return ""
var resp: String = item.get_metadata(COL.TEXT)
return resp
## 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