Compare commits
1 Commits
e76608a67e
...
plugin-loa
Author | SHA1 | Date | |
---|---|---|---|
ac9e94c9b0 |
365
Main.gd
Normal file
365
Main.gd
Normal 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
236
Main.tscn
Normal 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")
|
16
README.md
16
README.md
@ -4,18 +4,6 @@ Track your time, save it to a CSV file.
|
||||
|
||||

|
||||
|
||||
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.
@ -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 |
@ -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
|
@ -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")
|
@ -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
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -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]
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -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
34
main.gd
@ -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
249
plugin_loader.gd
Normal 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
9
plugin_v1.gd
Normal file
@ -0,0 +1,9 @@
|
||||
class_name Plugin_V1
|
||||
|
||||
|
||||
func plugin_start() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func plugin_stop() -> void:
|
||||
pass
|
@ -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"
|
||||
|
||||
|
@ -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)
|
@ -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)
|
@ -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"
|
@ -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]
|
@ -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
|
||||
|
||||
|
@ -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)
|
118
ui/Main.gd
118
ui/Main.gd
@ -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)
|
132
ui/Main.tscn
132
ui/Main.tscn
@ -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")
|
@ -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())
|
||||
)
|
||||
|
||||
|
||||
|
113
ui/settings.tscn
113
ui/settings.tscn
@ -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")
|
@ -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
|
Reference in New Issue
Block a user