complete refactoring
This commit is contained in:
		
							
								
								
									
										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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user