From 9aff9cbc19c44b1b97cacf02dcdd8a54a0f02a76 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 5 Mar 2022 02:02:31 +0000 Subject: Store database and option sets as JSON. Allow to parse options from CSV database or import from JSON file. --- logic/database.gd | 72 ++++++++++++++++++++++++++++++++++++++++--------------- logic/stage.gd | 41 ++++++++++++++++++------------- menu/menu.gd | 66 +++++++++++++++++++++++++++++++------------------- readme.md | 4 ++-- 4 files changed, 122 insertions(+), 61 deletions(-) diff --git a/logic/database.gd b/logic/database.gd index 2258f55..91328c5 100644 --- a/logic/database.gd +++ b/logic/database.gd @@ -1,7 +1,8 @@ extends TouchItemList class_name Database -const DATABASE_FILE_PATH: String = "user://database.csv" +const DATABASE_FILE_PATH: String = "user://database.json" +const DATABASE_FILE_VERSION: int = 1 var db: Array var selected_idx: int @@ -84,7 +85,7 @@ func delete_action_confirmed(): db.remove(selected_idx) self.remove_item(selected_idx) selected_idx = -1 - store_database() + save_database() clear_selection() @@ -125,7 +126,7 @@ func save_stage(entry: Dictionary): set_item_text(selected_idx, get_entry_view(db[selected_idx])) ensure_current_is_visible() - store_database() + save_database() staged_idx = -1 self.visible = true @@ -141,49 +142,84 @@ func discard_stage(): func load_database(file_path: String = DATABASE_FILE_PATH): var file := File.new() file.open(file_path, File.READ_WRITE) - var headers: PoolStringArray - var is_first_line := true + var file_content = file.get_as_text() + file.close() + var parse_result = JSON.parse(file_content) + + if parse_result.error != OK || typeof(parse_result.result) != TYPE_DICTIONARY: + push_error("Failed to parse database file: '%s'.") + return + + if parse_result.result["version"] != DATABASE_FILE_VERSION: + push_error("Invalid database file version '%s', expected '%s'." % DATABASE_FILE_VERSION) + return + + if typeof(parse_result.result["database"]) != TYPE_ARRAY: + push_error("Failed to load database file contents.") + return + + db = parse_result.result["database"] + + +func save_database(file_path: String = DATABASE_FILE_PATH): + var database_file := { + "version": DATABASE_FILE_VERSION, + "database": db, + } + var indentation_char := "" if file_path == DATABASE_FILE_PATH else "\t" + var file_content := JSON.print(database_file, indentation_char) + + var file := File.new() + file.open(file_path, File.WRITE) + file.store_string(file_content) + file.close() + + +static func import_database(file_path: String) -> Array: + var result: Array + var file := File.new() + file.open(file_path, File.READ_WRITE) + var headers := file.get_csv_line() while file.get_position() < file.get_len(): var csv_entry := file.get_csv_line() - if is_first_line: - is_first_line = false - headers = csv_entry - continue var entry = DatabaseEntry.instance_entry() for idx in headers.size(): var field_name := headers[idx] var field_value := csv_entry[idx] match field_name: + "date": + DatabaseEntry.set_entry_date(entry, field_value) "date_year", "date_month", "date_day": entry[field_name] = int(field_value) "is_urgency": entry[field_name] = true if field_value.strip_edges().to_lower() == "true" else false _: - entry[field_name] = field_value - db.append(entry) + if DatabaseEntry.ENTRY_PROTOTYPE.has(field_name): + entry[field_name] = field_value + if DatabaseEntry.is_valid_entry(entry): + result.append(entry) file.close() + return result -func store_database(file_path: String = DATABASE_FILE_PATH): +static func export_database(file_path: String, database: Array = db): var file := File.new() file.open(file_path, File.WRITE) var header := PoolStringArray(DatabaseEntry.ENTRY_PROTOTYPE.keys()) file.store_csv_line(header) - for it in db: + for it in database: # @DAM This approach depends on the order the dictionary fields are created. file.store_csv_line(it.values()) file.close() -func clear_database(save_changes: bool = false): +func clear_database(): clear_selection() self.clear() db.resize(0) - if save_changes: - store_database() -func fake_database(save_changes: bool = false): +func fake_database(): clear_database() for idx in range(500): var today := OS.get_date(true) @@ -199,7 +235,5 @@ func fake_database(save_changes: bool = false): }) db.append(fake_entry) self.add_item(get_entry_view(fake_entry)) - if save_changes: - store_database() diff --git a/logic/stage.gd b/logic/stage.gd index 892dee4..c0583b0 100644 --- a/logic/stage.gd +++ b/logic/stage.gd @@ -5,6 +5,7 @@ signal save # (database_entry: Dictionary) signal discard # () const OPTION_SETS_FILE_PATH: String = "user://option_sets.json" +const OPTION_SETS_FILE_VERSION: int = 1 const OPTION_SETS_NOT_AVAILABLE: String = "--" const OPTION_SETS_TREE_STRUCTURE := { "place": null, @@ -93,7 +94,7 @@ func save_action(): self.visible = false var staged_entry := get_stage() gather_option_sets(staged_entry) - store_option_sets() + save_option_sets() emit_signal("save", staged_entry) @@ -188,35 +189,43 @@ func gather_option_sets(entry: Dictionary, target: Dictionary = option_sets, blu gather_option_sets(entry, target[key][value], blueprint[key]) -func import_option_sets(file_path: String = OPTION_SETS_FILE_PATH): - load_option_sets(file_path) - sanitize_option_sets(option_sets) - store_option_sets() - - func load_option_sets(file_path: String = OPTION_SETS_FILE_PATH): var file := File.new() file.open(file_path, File.READ_WRITE) var file_content = file.get_as_text() file.close() var parse_result = JSON.parse(file_content) - if parse_result.error == OK && typeof(parse_result.result) == TYPE_DICTIONARY: - option_sets = parse_result.result - else: - option_sets = {} + + if parse_result.error != OK || typeof(parse_result.result) != TYPE_DICTIONARY: push_error("Failed to parse option sets file: '%s'.") + return + + if parse_result.result["version"] != OPTION_SETS_FILE_VERSION: + push_error("Invalid option sets file version '%s', expected '%s'." % OPTION_SETS_FILE_VERSION) + return + + if typeof(parse_result.result["option_sets"]) != TYPE_DICTIONARY: + push_error("Failed to load option sets file contents.") + return + + option_sets = parse_result.result["option_sets"] -func store_option_sets(file_path: String = OPTION_SETS_FILE_PATH): +func save_option_sets(file_path: String = OPTION_SETS_FILE_PATH): + var option_sets_file := { + "version": OPTION_SETS_FILE_VERSION, + "option_sets": option_sets, + } + var indentation_char := "" if file_path == OPTION_SETS_FILE_PATH else "\t" + var file_content := JSON.print(option_sets_file, indentation_char) + var file := File.new() file.open(file_path, File.WRITE) - file.store_string(JSON.print(option_sets, "" if file_path == OPTION_SETS_FILE_PATH else "\t")) + file.store_string(file_content) file.close() -func clear_option_sets(save_changes: bool = false): +func clear_option_sets(): option_sets = {} - if save_changes: - store_option_sets() diff --git a/menu/menu.gd b/menu/menu.gd index 5847d5b..9a5bdbb 100644 --- a/menu/menu.gd +++ b/menu/menu.gd @@ -1,13 +1,13 @@ extends MenuButton const menu_items: Array = [ - { label = "IMPORT OPTION SETS", action = "_menu_import_option_sets_action" }, - { label = "EXPORT OPTION SETS", action = "_menu_export_option_sets_action" }, - { label = "CLEAR OPTION SETS", action = "_menu_clear_option_sets_action" }, - { label = "EXPORT DATA", action = "_menu_export_data_action" }, - { label = "CLEAR DATA", action = "_menu_clear_data_action" }, - { label = "ABOUT", action = "_menu_about_action" }, - { label = "FAKE_DB", action = "_menu_fake_db_action" }, + { label = "IMPORT OPTION SETS", action = "import_option_sets_action" }, + { label = "EXPORT OPTION SETS", action = "export_option_sets_action" }, + { label = "CLEAR OPTION SETS", action = "clear_option_sets_action" }, + { label = "EXPORT DATA", action = "export_data_action" }, + { label = "CLEAR DATA", action = "clear_data_action" }, + { label = "ABOUT", action = "about_action" }, + { label = "TEST_FAKE_DB", action = "test_fake_db_action" }, ] const license_font_b612: String = "res://licenses/font_b612.txt" const license_godot: String = "res://licenses/godot.txt" @@ -29,64 +29,82 @@ func id_pressed(id: int): self.call_deferred(menu_items[id].action) -func _menu_import_option_sets_action(): +func import_option_sets_action(): file_picker.window_title = "IMPORT OPTION SETS" file_picker.mode = FileDialog.MODE_OPEN_FILE file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) - file_picker.filters[0] = "*.json" + file_picker.filters = ["*.json", "*.csv"] file_picker.current_file = "" - file_picker.connect("file_selected", stage, "import_option_sets", [true], CONNECT_ONESHOT) + file_picker.connect("file_selected", self, "import_option_sets_action_confirmed", [], CONNECT_ONESHOT) file_picker.show_modal(true) file_picker.invalidate() -func _menu_export_option_sets_action(): +func import_option_sets_action_confirmed(file_path: String): + match file_path.get_extension(): + "json": + stage.load_option_sets(file_path) + stage.sanitize_option_sets(stage.option_sets) + stage.save_option_sets() + + "csv": + var database := Database.import_database(file_path) + for it in database: + stage.gather_option_sets(it) + stage.save_option_sets() + + _: + push_error("Invalid file extension selected to be parsed for option sets: '%s'." % file_path.get_file()) + return + + +func export_option_sets_action(): file_picker.window_title = "EXPORT OPTION SETS" file_picker.mode = FileDialog.MODE_SAVE_FILE file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) - file_picker.filters[0] = "*.json" + file_picker.filters = ["*.json"] file_picker.current_file = "" - file_picker.connect("file_selected", stage, "store_option_sets", [], CONNECT_ONESHOT) + file_picker.connect("file_selected", stage, "save_option_sets", [], CONNECT_ONESHOT) file_picker.show_modal(true) file_picker.invalidate() -func _menu_clear_option_sets_action(): +func clear_option_sets_action(): confirm_action.window_title = "CLEAR OPTION SETS" confirm_action.dialog_text = "Do you want to delete all option sets?" - confirm_action.connect("confirmed", stage, "clear_option_sets", [true], CONNECT_ONESHOT) + confirm_action.connect("confirmed", stage, "clear_option_sets", [], CONNECT_ONESHOT) confirm_action.show_modal(true) -func _menu_export_data_action(): +func export_data_action(): file_picker.window_title = "EXPORT DATA" file_picker.mode = FileDialog.MODE_SAVE_FILE file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) - file_picker.filters[0] = "*.csv" + file_picker.filters = ["*.csv"] file_picker.current_file = "" - file_picker.connect("file_selected", database, "store_database", [], CONNECT_ONESHOT) + file_picker.connect("file_selected", database, "save_database", [], CONNECT_ONESHOT) file_picker.show_modal(true) file_picker.invalidate() -func _menu_clear_data_action(): +func clear_data_action(): confirm_action.window_title = "CLEAR DATA" confirm_action.dialog_text = "Do you want to delete all entries from the database?" - confirm_action.connect("confirmed", database, "clear_database", [true], CONNECT_ONESHOT) + confirm_action.connect("confirmed", database, "clear_database", [], CONNECT_ONESHOT) confirm_action.show_modal(true) -func _menu_about_action(): +func about_action(): confirm_action.window_title = "ABOUT" confirm_action.dialog_text = "About text here!" confirm_action.show_modal(true) # @DAM Hide this debug method before release. -func _menu_fake_db_action(): - confirm_action.window_title = "FAKE DB" +func test_fake_db_action(): + confirm_action.window_title = "TEST FAKE DB" confirm_action.dialog_text = "Do you want to delete all entries from the database and replace by fake entries?" - confirm_action.connect("confirmed", database, "fake_database", [true], CONNECT_ONESHOT) + confirm_action.connect("confirmed", database, "fake_database", [], CONNECT_ONESHOT) confirm_action.show_modal(true) diff --git a/readme.md b/readme.md index bc01851..371eee6 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,8 @@ Surgery Log - [x] The stage control must be set to ignore the mouse, otherwise the touch-sensor conflicts with the built-in scroll; - [x] On database, selecting an entry and removing it will leave the action buttons visible while no entry is selected; - [x] Tweak 'POINTER_VELOCITY_DECAYING_FACTOR' and 'POINTER_VELOCITY_BOOST_FACTOR' on database and stage screens; -- [ ] Allow to parse option sets from database file; +- [x] Allow to parse option sets from database file; +- [x] Check if import_option_sets, store_option_sets, store_database require the parameter save_changes; this requires changes on databse, stage and menu scripts; - [ ] Fix back button: - on stage screen should show pop-up asking it changes are to be discarded; - on file-pickers screen should close them; @@ -55,7 +56,6 @@ Surgery Log - on database screen, should deselect selected item, otherwise should quit the app; - [ ] Hide dialogs title bar (appear in the top with 1 or 2 pixels height); - [ ] Database menu and action buttons are not nice; Improve appearance; -- [ ] Check if import_option_sets, store_option_sets, store_database require the parameter save_changes; this requires changes on databse, stage and menu scripts; - [ ] Improve menu appearance; - [ ] Setup two themes: - [ ] theme_light -- cgit v1.2.3