complete refactoring
This commit is contained in:
parent
929fa76329
commit
5209f26ec5
277
Main.gd
277
Main.gd
@ -1,22 +1,10 @@
|
|||||||
extends Control
|
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 time_label: Label = %TimeLabel
|
||||||
@onready var start_button: Button = %StartButton
|
@onready var start_button: Button = %StartButton
|
||||||
@onready var task_name_line_edit: LineEdit = %TaskNameLineEdit
|
@onready var task_name_line_edit: LineEdit = %TaskNameLineEdit
|
||||||
@onready var previous_tasks_tree: Tree = %PreviousTasksTree
|
@onready var time_entries_items_tree: TimeEntriesItemsTree = %TimeEntriesItemsTree
|
||||||
@onready var timer: Timer = %Timer
|
@onready var timer: Timer = %Timer
|
||||||
@onready var tasks_button: Button = %TasksButton
|
@onready var tasks_button: Button = %TasksButton
|
||||||
@onready var settings_button: Button = %SettingsButton
|
@onready var settings_button: Button = %SettingsButton
|
||||||
@ -33,20 +21,14 @@ const STRINGS := {
|
|||||||
@onready var open_data_dir_button: Button = %OpenDataDirButton
|
@onready var open_data_dir_button: Button = %OpenDataDirButton
|
||||||
|
|
||||||
|
|
||||||
var previous_entries: Array[TimeEntry] = []
|
var timesheet: TimeSheet
|
||||||
var current_entry := TimeEntry.new()
|
var config: ConfigManager = preload("res://config_manager.tres")
|
||||||
var current_item: TreeItem
|
|
||||||
var config := ConfigFile.new()
|
|
||||||
var current_file := ""
|
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
|
|
||||||
config.load(CONFIG_PATH)
|
if config.theme_path:
|
||||||
var default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv")
|
var new_theme: Theme = ResourceLoader.load(config.theme_path, "Theme")
|
||||||
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:
|
if new_theme != null:
|
||||||
theme = new_theme
|
theme = new_theme
|
||||||
|
|
||||||
@ -54,7 +36,7 @@ func _init() -> void:
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
||||||
get_tree().set_auto_accept_quit(false)
|
get_tree().set_auto_accept_quit(false)
|
||||||
var _root := previous_tasks_tree.create_item()
|
var _root := time_entries_items_tree.create_item()
|
||||||
|
|
||||||
file_path_button.pressed.connect(
|
file_path_button.pressed.connect(
|
||||||
file_path_file_dialog.popup_centered
|
file_path_file_dialog.popup_centered
|
||||||
@ -71,8 +53,7 @@ func _ready() -> void:
|
|||||||
var new_theme: Theme = ResourceLoader.load(theme_path, "Theme")
|
var new_theme: Theme = ResourceLoader.load(theme_path, "Theme")
|
||||||
if new_theme != null:
|
if new_theme != null:
|
||||||
theme = new_theme
|
theme = new_theme
|
||||||
config.set_value("MAIN", "theme", theme_path)
|
config.theme_path = theme_path
|
||||||
config.save(CONFIG_PATH)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
theme_path_file_dialog.hide()
|
theme_path_file_dialog.hide()
|
||||||
@ -82,46 +63,37 @@ func _ready() -> void:
|
|||||||
|
|
||||||
timer.timeout.connect(
|
timer.timeout.connect(
|
||||||
func on_timer_timeout() -> void:
|
func on_timer_timeout() -> void:
|
||||||
current_entry.update()
|
timesheet.update()
|
||||||
|
|
||||||
time_label.text = current_entry.get_period()
|
time_label.text = timesheet.get_period()
|
||||||
|
|
||||||
var total_elapsed: int = current_entry.get_total_elapsed_seconds()
|
var total_elapsed: int = timesheet.get_total_elapsed_seconds()
|
||||||
current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed))
|
time_entries_items_tree.set_time_elapsed(total_elapsed)
|
||||||
current_item.set_metadata(COL.TIME, total_elapsed)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
start_button.tooltip_text = tr(STRINGS.START)
|
start_button.tooltip_text = tr(Consts.START)
|
||||||
start_button.toggle_mode = true
|
start_button.toggle_mode = true
|
||||||
start_button.toggled.connect(
|
start_button.toggled.connect(
|
||||||
func start(is_on: bool) -> void:
|
func start(is_on: bool) -> void:
|
||||||
if sound_check_box.button_pressed:
|
if sound_check_box.button_pressed:
|
||||||
audio_stream_player.play()
|
audio_stream_player.play()
|
||||||
if is_on:
|
if is_on:
|
||||||
current_entry = TimeEntry.new().start_recording()
|
timesheet.start_entry(task_name_line_edit.text)
|
||||||
current_entry.name = task_name_line_edit.text
|
set_button_as_started()
|
||||||
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:
|
else:
|
||||||
add_to_entries()
|
timesheet.close_entry()
|
||||||
start_button.tooltip_text = tr(STRINGS.START)
|
set_button_as_stopped()
|
||||||
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 = config.last_task_name
|
||||||
task_name_line_edit.text_changed.connect(
|
task_name_line_edit.text_changed.connect(
|
||||||
func(new_text: String) -> void:
|
func(new_text: String) -> void:
|
||||||
config.set_value("MAIN", "last_task_name", new_text)
|
config.last_task_name = new_text
|
||||||
config.save(CONFIG_PATH)
|
|
||||||
)
|
)
|
||||||
previous_tasks_tree.item_selected.connect(
|
time_entries_items_tree.item_selected.connect(
|
||||||
func item_selected() -> void:
|
func item_selected() -> void:
|
||||||
var item := previous_tasks_tree.get_selected()
|
task_name_line_edit.text = time_entries_items_tree.get_current_text()
|
||||||
task_name_line_edit.text = item.get_metadata(COL.TEXT)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tasks_button.toggle_mode = true
|
tasks_button.toggle_mode = true
|
||||||
@ -146,10 +118,10 @@ func _ready() -> void:
|
|||||||
settings_window.hide()
|
settings_window.hide()
|
||||||
)
|
)
|
||||||
|
|
||||||
sound_check_box.button_pressed = config.get_value("MAIN", "sound_fx", true)
|
sound_check_box.button_pressed = config.sound_fx_on
|
||||||
sound_check_box.toggled.connect(
|
sound_check_box.toggled.connect(
|
||||||
func sound_toggle(is_on: bool) -> void:
|
func sound_toggle(is_on: bool) -> void:
|
||||||
config.set_value("MAIN", "sound_fx", is_on)
|
config.sound_fx_on = is_on
|
||||||
)
|
)
|
||||||
|
|
||||||
open_data_dir_button.pressed.connect(
|
open_data_dir_button.pressed.connect(
|
||||||
@ -158,208 +130,47 @@ func _ready() -> void:
|
|||||||
|
|
||||||
attributions_rich_text_label.meta_clicked.connect(OS.shell_open)
|
attributions_rich_text_label.meta_clicked.connect(OS.shell_open)
|
||||||
|
|
||||||
set_current_file(current_file)
|
set_current_file(config.current_file)
|
||||||
|
|
||||||
|
|
||||||
## Adds a new time entry to the tree and to the data file
|
func set_button_as_started() -> void:
|
||||||
func add_to_entries() -> void:
|
time_entries_items_tree.set_current_item(timesheet.current_entry.name)
|
||||||
previous_entries.append(current_entry)
|
start_button.tooltip_text = tr(Consts.STOP)
|
||||||
var file := FileAccess.open(current_file, FileAccess.WRITE)
|
start_button.theme_type_variation = Consts.THEME_OVERRIDE_STOP
|
||||||
if file == null:
|
timer.start()
|
||||||
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 set_button_as_stopped() -> void:
|
||||||
func append_name_to_tree(task_name: String, total_time := 0) -> TreeItem:
|
start_button.tooltip_text = tr(Consts.START)
|
||||||
var item := previous_tasks_tree.get_root()
|
time_label.text = Consts.NO_TIME
|
||||||
for item_name in task_name.split("/"):
|
start_button.theme_type_variation = Consts.THEME_OVERRIDE_START
|
||||||
item.collapsed = false
|
timer.stop()
|
||||||
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
|
## Changes the data file path, and loads the new path
|
||||||
func set_current_file(new_current_file: String) -> void:
|
func set_current_file(new_current_file: String) -> void:
|
||||||
if not load_file(new_current_file):
|
timesheet = TimeSheet.restore(new_current_file)
|
||||||
|
if timesheet == null:
|
||||||
return
|
return
|
||||||
previous_entries.clear()
|
config.current_file = new_current_file
|
||||||
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
|
|
||||||
|
|
||||||
|
for entry_name in timesheet.entries_names:
|
||||||
|
time_entries_items_tree.append_name_to_tree(entry_name, timesheet.entries_names[entry_name])
|
||||||
|
|
||||||
## Loads the data file
|
if timesheet.current_entry != null and timesheet.current_entry.closed == false:
|
||||||
func load_file(source_path: String) -> bool:
|
start_button.set_pressed_no_signal(true)
|
||||||
var file := FileAccess.open(source_path, FileAccess.READ)
|
set_button_as_started()
|
||||||
if file == null:
|
file_path_file_dialog.current_path = config.current_file
|
||||||
file = FileAccess.open(source_path, FileAccess.WRITE)
|
file_path_file_dialog.current_dir = config.current_file.get_base_dir()
|
||||||
if file == null:
|
file_path_line_edit.text = config.current_file
|
||||||
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:
|
func _notification(what: int) -> void:
|
||||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
if start_button.button_pressed:
|
|
||||||
add_to_entries()
|
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
## Unused; if a manual quit button is added, this would be used
|
## Unused; if a manual quit button is added, this would be used
|
||||||
func quit() -> void:
|
func quit() -> void:
|
||||||
get_tree().notification(NOTIFICATION_WM_CLOSE_REQUEST)
|
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)
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
[gd_scene load_steps=6 format=3 uid="uid://bmlciwscreowf"]
|
[gd_scene load_steps=7 format=3 uid="uid://bmlciwscreowf"]
|
||||||
|
|
||||||
[ext_resource type="Theme" uid="uid://bd8ancgbfsvmd" path="res://assets/default_theme.theme" id="1_1mila"]
|
[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="Script" path="res://Main.gd" id="1_vrowr"]
|
||||||
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="3_o37py"]
|
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="3_o37py"]
|
||||||
|
[ext_resource type="Script" path="res://ui/time_entries_items_tree.gd" id="3_wwscb"]
|
||||||
|
|
||||||
[sub_resource type="InputEventKey" id="InputEventKey_guuii"]
|
[sub_resource type="InputEventKey" id="InputEventKey_guuii"]
|
||||||
device = -1
|
device = -1
|
||||||
@ -125,12 +126,13 @@ theme_override_constants/margin_top = 20
|
|||||||
theme_override_constants/margin_right = 20
|
theme_override_constants/margin_right = 20
|
||||||
theme_override_constants/margin_bottom = 20
|
theme_override_constants/margin_bottom = 20
|
||||||
|
|
||||||
[node name="PreviousTasksTree" type="Tree" parent="Main/PreviousTasksWindow/PanelContainer/MarginContainer"]
|
[node name="TimeEntriesItemsTree" type="Tree" parent="Main/PreviousTasksWindow/PanelContainer/MarginContainer"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
columns = 2
|
columns = 2
|
||||||
|
script = ExtResource("3_wwscb")
|
||||||
|
|
||||||
[node name="SettingsWindow" type="Window" parent="Main"]
|
[node name="SettingsWindow" type="Window" parent="Main"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -3,15 +3,15 @@
|
|||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://cpmyyivxx0dlt"
|
uid="uid://cpmyyivxx0dlt"
|
||||||
path="res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"
|
path="res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://logo.png"
|
source_file="res://assets/logo.png"
|
||||||
dest_files=["res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"]
|
dest_files=["res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -3,15 +3,15 @@
|
|||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://fk5s6m8qlsei"
|
uid="uid://fk5s6m8qlsei"
|
||||||
path="res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"
|
path="res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://logo.svg"
|
source_file="res://assets/logo.svg"
|
||||||
dest_files=["res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"]
|
dest_files=["res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
57
assets/stop.svg
Normal file
57
assets/stop.svg
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 1.6 KiB |
37
assets/stop.svg.import
Normal file
37
assets/stop.svg.import
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[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
|
51
config_manager.gd
Normal file
51
config_manager.gd
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
class_name ConfigManager extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
const CONFIG_PATH := "user://settings.cfg"
|
||||||
|
var _config := ConfigFile.new()
|
||||||
|
|
||||||
|
|
||||||
|
var current_file: String = "":
|
||||||
|
set(value):
|
||||||
|
current_file = value
|
||||||
|
_config.set_value("MAIN", "file", value)
|
||||||
|
save()
|
||||||
|
get:
|
||||||
|
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_path: String = "":
|
||||||
|
set(value):
|
||||||
|
theme_path = value
|
||||||
|
_config.set_value("MAIN", "theme", value)
|
||||||
|
save()
|
||||||
|
get:
|
||||||
|
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)
|
6
config_manager.tres
Normal file
6
config_manager.tres
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ConfigManager" load_steps=2 format=3 uid="uid://e3yofdasfcli"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://config_manager.gd" id="1_xfu8y"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_xfu8y")
|
@ -25,7 +25,6 @@ texture_format/bptc=false
|
|||||||
texture_format/s3tc=true
|
texture_format/s3tc=true
|
||||||
texture_format/etc=false
|
texture_format/etc=false
|
||||||
texture_format/etc2=false
|
texture_format/etc2=false
|
||||||
texture_format/no_bptc_fallbacks=true
|
|
||||||
binary_format/architecture="x86_64"
|
binary_format/architecture="x86_64"
|
||||||
ssh_remote_deploy/enabled=false
|
ssh_remote_deploy/enabled=false
|
||||||
ssh_remote_deploy/host="user@host_ip"
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
@ -50,7 +49,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="exports/Rat Times.app"
|
export_path="exports/Rat Times.zip"
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
encrypt_pck=false
|
encrypt_pck=false
|
||||||
@ -67,7 +66,7 @@ application/icon="res://logo.icns"
|
|||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/bundle_identifier="org.mutnt.io.rat-times"
|
application/bundle_identifier="org.mutnt.io.rat-times"
|
||||||
application/signature=""
|
application/signature=""
|
||||||
application/app_category="Productivity"
|
application/app_category="Developer-tools"
|
||||||
application/short_version="1.0"
|
application/short_version="1.0"
|
||||||
application/version="1.0"
|
application/version="1.0"
|
||||||
application/copyright=""
|
application/copyright=""
|
||||||
@ -361,7 +360,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path=""
|
export_path="exports/rat-times.ipa"
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
encrypt_pck=false
|
encrypt_pck=false
|
||||||
@ -454,7 +453,6 @@ texture_format/bptc=false
|
|||||||
texture_format/s3tc=true
|
texture_format/s3tc=true
|
||||||
texture_format/etc=false
|
texture_format/etc=false
|
||||||
texture_format/etc2=false
|
texture_format/etc2=false
|
||||||
texture_format/no_bptc_fallbacks=true
|
|
||||||
binary_format/architecture="x86_64"
|
binary_format/architecture="x86_64"
|
||||||
codesign/enable=false
|
codesign/enable=false
|
||||||
codesign/identity_type=0
|
codesign/identity_type=0
|
||||||
|
@ -16,9 +16,10 @@ run/main_scene="res://Main.tscn"
|
|||||||
config/use_custom_user_dir=true
|
config/use_custom_user_dir=true
|
||||||
config/custom_user_dir_name="rat_times"
|
config/custom_user_dir_name="rat_times"
|
||||||
config/features=PackedStringArray("4.0")
|
config/features=PackedStringArray("4.0")
|
||||||
|
run/low_processor_mode=true
|
||||||
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
|
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
|
||||||
boot_splash/show_image=false
|
boot_splash/show_image=false
|
||||||
config/icon="res://logo.svg"
|
config/icon="res://assets/logo.svg"
|
||||||
config/macos_native_icon="res://logo.icns"
|
config/macos_native_icon="res://logo.icns"
|
||||||
config/windows_native_icon="res://logo.ico"
|
config/windows_native_icon="res://logo.ico"
|
||||||
|
|
||||||
|
8
scripts/consts.gd
Normal file
8
scripts/consts.gd
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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"
|
77
scripts/time_entry.gd
Normal file
77
scripts/time_entry.gd
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
## 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
|
||||||
|
|
84
scripts/time_sheet.gd
Normal file
84
scripts/time_sheet.gd
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
54
scripts/time_stamp.gd
Normal file
54
scripts/time_stamp.gd
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
## 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)
|
52
ui/time_entries_items_tree.gd
Normal file
52
ui/time_entries_items_tree.gd
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
class_name TimeEntriesItemsTree extends Tree
|
||||||
|
|
||||||
|
enum COL{
|
||||||
|
TEXT,
|
||||||
|
TIME
|
||||||
|
}
|
||||||
|
|
||||||
|
var current_item: TreeItem
|
||||||
|
|
||||||
|
|
||||||
|
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_time := 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)
|
||||||
|
if not item.get_metadata(COL.TIME):
|
||||||
|
item.set_metadata(COL.TIME, total_time)
|
||||||
|
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
|
Loading…
Reference in New Issue
Block a user