diff --git a/core/Services/Database.vala b/core/Services/Database.vala index df36dd75a..ffd7ab5f6 100644 --- a/core/Services/Database.vala +++ b/core/Services/Database.vala @@ -25,6 +25,8 @@ public class Services.Database : GLib.Object { private string errormsg; private string sql; + private Gee.HashMap> table_columns = new Gee.HashMap>(); + public signal void opened (); public signal void reset (); @@ -37,9 +39,136 @@ public class Services.Database : GLib.Object { return _instance; } + construct { + table_columns ["Attachments"] = new Gee.ArrayList (); + table_columns ["Attachments"].add ("id"); + table_columns ["Attachments"].add ("item_id"); + table_columns ["Attachments"].add ("file_type"); + table_columns ["Attachments"].add ("file_name"); + table_columns ["Attachments"].add ("file_size"); + table_columns ["Attachments"].add ("file_path"); + + table_columns ["CurTempIds"] = new Gee.ArrayList (); + table_columns ["CurTempIds"].add ("id"); + table_columns ["CurTempIds"].add ("temp_id"); + table_columns ["CurTempIds"].add ("object"); + + table_columns ["Items"] = new Gee.ArrayList (); + table_columns ["Items"].add ("id"); + table_columns ["Items"].add ("content"); + table_columns ["Items"].add ("description"); + table_columns ["Items"].add ("due"); + table_columns ["Items"].add ("added_at"); + table_columns ["Items"].add ("completed_at"); + table_columns ["Items"].add ("updated_at"); + table_columns ["Items"].add ("section_id"); + table_columns ["Items"].add ("project_id"); + table_columns ["Items"].add ("parent_id"); + table_columns ["Items"].add ("priority"); + table_columns ["Items"].add ("child_order"); + table_columns ["Items"].add ("checked"); + table_columns ["Items"].add ("is_deleted"); + table_columns ["Items"].add ("day_order"); + table_columns ["Items"].add ("collapsed"); + table_columns ["Items"].add ("pinned"); + table_columns ["Items"].add ("labels"); + table_columns ["Items"].add ("extra_data"); + table_columns ["Items"].add ("item_type"); + + table_columns ["Labels"] = new Gee.ArrayList (); + table_columns ["Labels"].add ("id"); + table_columns ["Labels"].add ("name"); + table_columns ["Labels"].add ("color"); + table_columns ["Labels"].add ("item_order"); + table_columns ["Labels"].add ("is_deleted"); + table_columns ["Labels"].add ("is_favorite"); + table_columns ["Labels"].add ("backend_type"); + table_columns ["Labels"].add ("source_id"); + + table_columns ["OEvents"] = new Gee.ArrayList (); + table_columns ["OEvents"].add ("id"); + table_columns ["OEvents"].add ("event_type"); + table_columns ["OEvents"].add ("event_date"); + table_columns ["OEvents"].add ("object_id"); + table_columns ["OEvents"].add ("object_type"); + table_columns ["OEvents"].add ("object_key"); + table_columns ["OEvents"].add ("object_old_value"); + table_columns ["OEvents"].add ("object_new_value"); + table_columns ["OEvents"].add ("parent_item_id"); + table_columns ["OEvents"].add ("parent_project_id"); + + table_columns ["Projects"] = new Gee.ArrayList (); + table_columns ["Projects"].add ("id"); + table_columns ["Projects"].add ("name"); + table_columns ["Projects"].add ("color"); + table_columns ["Projects"].add ("backend_type"); + table_columns ["Projects"].add ("inbox_project"); + table_columns ["Projects"].add ("team_inbox"); + table_columns ["Projects"].add ("child_order"); + table_columns ["Projects"].add ("is_deleted"); + table_columns ["Projects"].add ("is_archived"); + table_columns ["Projects"].add ("is_favorite"); + table_columns ["Projects"].add ("shared"); + table_columns ["Projects"].add ("view_style"); + table_columns ["Projects"].add ("sort_order"); + table_columns ["Projects"].add ("parent_id"); + table_columns ["Projects"].add ("collapsed"); + table_columns ["Projects"].add ("icon_style"); + table_columns ["Projects"].add ("emoji"); + table_columns ["Projects"].add ("show_completed"); + table_columns ["Projects"].add ("description"); + table_columns ["Projects"].add ("due_date"); + table_columns ["Projects"].add ("inbox_section_hidded"); + table_columns ["Projects"].add ("sync_id"); + table_columns ["Projects"].add ("source_id"); + + table_columns ["Queue"] = new Gee.ArrayList (); + table_columns ["Queue"].add ("uuid"); + table_columns ["Queue"].add ("object_id"); + table_columns ["Queue"].add ("query"); + table_columns ["Queue"].add ("temp_id"); + table_columns ["Queue"].add ("args"); + table_columns ["Queue"].add ("date_added"); + + table_columns ["Reminders"] = new Gee.ArrayList (); + table_columns ["Reminders"].add ("id"); + table_columns ["Reminders"].add ("notify_uid"); + table_columns ["Reminders"].add ("item_id"); + table_columns ["Reminders"].add ("service"); + table_columns ["Reminders"].add ("type"); + table_columns ["Reminders"].add ("due"); + table_columns ["Reminders"].add ("mm_offset"); + table_columns ["Reminders"].add ("is_deleted"); + + table_columns ["Sections"] = new Gee.ArrayList (); + table_columns ["Sections"].add ("id"); + table_columns ["Sections"].add ("name"); + table_columns ["Sections"].add ("archived_at"); + table_columns ["Sections"].add ("added_at"); + table_columns ["Sections"].add ("project_id"); + table_columns ["Sections"].add ("section_order"); + table_columns ["Sections"].add ("collapsed"); + table_columns ["Sections"].add ("is_deleted"); + table_columns ["Sections"].add ("is_archived"); + table_columns ["Sections"].add ("color"); + table_columns ["Sections"].add ("description"); + table_columns ["Sections"].add ("hidded"); + + table_columns ["Sources"] = new Gee.ArrayList (); + table_columns ["Sources"].add ("id"); + table_columns ["Sources"].add ("source_type"); + table_columns ["Sources"].add ("display_name"); + table_columns ["Sources"].add ("added_at"); + table_columns ["Sources"].add ("updated_at"); + table_columns ["Sources"].add ("is_visible"); + table_columns ["Sources"].add ("child_order"); + table_columns ["Sources"].add ("sync_server"); + table_columns ["Sources"].add ("last_sync"); + table_columns ["Sources"].add ("data"); + } + public void init_database () { db_path = Environment.get_user_data_dir () + "/io.github.alainm23.planify/database.db"; - print ("DB: %s\n".printf (db_path)); Sqlite.Database.open (db_path, out db); create_tables (); @@ -440,7 +569,6 @@ public class Services.Database : GLib.Object { /* * Planner 3.10 * - Add description to Projects - * - Add due date to Projects */ add_text_column ("Projects", "description", ""); @@ -500,6 +628,75 @@ public class Services.Database : GLib.Object { } } + public bool verify_integrity () { + Sqlite.Statement stmt; + + // Verify Data Integrity + sql = """ + PRAGMA integrity_check; + """; + + db.prepare_v2 (sql, sql.length, out stmt); + + if (stmt.step () == Sqlite.ROW) { + if (stmt.column_text (0) != "ok") { + return false; + } + } + + // Verify Tables Integrity + string[] tables = { "Attachments", "CurTempIds", "Items", "Labels", + "OEvents", "Projects", "Queue", "Reminders", "Sections", "Sources" }; + + foreach (var table_name in tables) { + if (!table_exists (table_name)) { + return false; + } + } + + // Verify Table Columns + foreach (var table in table_columns.keys) { + if (!table_columns_exists (table, table_columns.get (table))) { + return false; + } + } + + return true; + } + + private bool table_exists (string table_name) { + Sqlite.Statement stmt; + + sql = """ + SELECT name FROM sqlite_master WHERE type='table' AND name='%s'; + """.printf (table_name); + + db.prepare_v2 (sql, sql.length, out stmt); + + return stmt.step () == Sqlite.ROW; + } + + private bool table_columns_exists (string table, Gee.ArrayList columns) { + Sqlite.Statement stmt; + + sql = """ + PRAGMA table_info(%s); + """.printf (table); + + db.prepare_v2 (sql, sql.length, out stmt); + + stmt.step (); + + while (stmt.step () == Sqlite.ROW) { + if (!columns.contains (stmt.column_text (1))) { + print ("Falta: %s: %s\n".printf (table, stmt.column_text (1))); + return false; + } + } + + return true; + } + /* Sources */ @@ -1598,7 +1795,6 @@ public class Services.Database : GLib.Object { returned = stmt.column_int (0) > 0; } - return returned; } @@ -1924,7 +2120,6 @@ public class Services.Database : GLib.Object { public bool column_exists (string table, string column) { Sqlite.Statement stmt; - bool returned = false; sql = """ PRAGMA table_info(%s); @@ -1936,12 +2131,11 @@ public class Services.Database : GLib.Object { while (stmt.step () == Sqlite.ROW) { if (stmt.column_text (1) == column) { - returned = true; + return true; } } - - return returned; + return false; } public void add_text_column (string table, string column, string default_value) { diff --git a/data/io.github.alainm23.planify.gresource.xml b/data/io.github.alainm23.planify.gresource.xml index 0d6340082..aee2f2906 100644 --- a/data/io.github.alainm23.planify.gresource.xml +++ b/data/io.github.alainm23.planify.gresource.xml @@ -98,6 +98,7 @@ resources/icons/delay-symbolic.svg resources/icons/go-up-symbolic.svg resources/icons/go-next-symbolic.svg + resources/icons/process-error-symbolic.svg @@ -170,6 +171,7 @@ resources/icons/delay-symbolic.svg resources/icons/go-up-symbolic.svg resources/icons/go-next-symbolic.svg + resources/icons/process-error-symbolic.svg @@ -242,5 +244,6 @@ resources/icons/delay-symbolic.svg resources/icons/go-up-symbolic.svg resources/icons/go-next-symbolic.svg + resources/icons/process-error-symbolic.svg diff --git a/data/resources/icons/process-error-symbolic.svg b/data/resources/icons/process-error-symbolic.svg new file mode 100644 index 000000000..e111bf146 --- /dev/null +++ b/data/resources/icons/process-error-symbolic.svg @@ -0,0 +1,55 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/MainWindow.vala b/src/MainWindow.vala index f8c243d9f..53944da3f 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -31,6 +31,8 @@ public class MainWindow : Adw.ApplicationWindow { private Widgets.ContextMenu.MenuItem archive_item; private Widgets.ContextMenu.MenuSeparator archive_separator; private Adw.ToastOverlay toast_overlay; + private Adw.ViewStack view_stack; + private Gtk.Widget error_db_page; public Services.ActionManager action_manager; @@ -131,9 +133,15 @@ public class MainWindow : Adw.ApplicationWindow { var breakpoint = new Adw.Breakpoint (Adw.BreakpointCondition.parse ("max-width: 800sp")); breakpoint.add_setter (overlay_split_view, "collapsed", true); add_breakpoint (breakpoint); - - content = overlay_split_view; + error_db_page = build_error_db_page (); + + view_stack = new Adw.ViewStack (); + view_stack.add (overlay_split_view); + view_stack.add (error_db_page); + + content = view_stack; + Services.Settings.get_default ().settings.bind ("pane-position", overlay_split_view, "min_sidebar_width", GLib.SettingsBindFlags.DEFAULT); Services.Settings.get_default ().settings.bind ("slim-mode", overlay_split_view, "show_sidebar", GLib.SettingsBindFlags.DEFAULT); Services.Settings.get_default ().settings.bind ("mobile-mode", overlay_split_view, "collapsed", GLib.SettingsBindFlags.DEFAULT); @@ -145,6 +153,63 @@ public class MainWindow : Adw.ApplicationWindow { return GLib.Source.REMOVE; }); + Services.Database.get_default ().opened.connect (() => { + if (!Services.Database.get_default ().verify_integrity ()) { + view_stack.visible_child = error_db_page; + return; + } + + view_stack.visible_child = overlay_split_view; + + if (Services.Store.instance ().is_sources_empty ()) { + Util.get_default ().create_local_source (); + } + + if (Services.Store.instance ().is_database_empty ()) { + Util.get_default ().create_inbox_project (); + Util.get_default ().create_tutorial_project (); + Util.get_default ().create_default_labels (); + } + + sidebar.init (); + + Services.Notification.get_default (); + Services.TimeMonitor.get_default ().init_timeout (); + + go_homepage (); + + Services.Store.instance ().project_deleted.connect (valid_view_removed); + Services.Store.instance ().project_archived.connect (valid_view_removed); + + check_archived (); + + Services.DBusServer.get_default ().item_added.connect ((id) => { + Objects.Item item = Services.Database.get_default ().get_item_by_id (id); + Gee.ArrayList reminders = Services.Database.get_default ().get_reminders_by_item_id (id); + + Services.Store.instance ().add_item (item); + foreach (Objects.Reminder reminder in reminders) { + item.add_reminder_events (reminder); + } + }); + + Timeout.add (Constants.SYNC_TIMEOUT, () => { + foreach (Objects.Source source in Services.Store.instance ().sources) { + source.run_server (); + } + + return GLib.Source.REMOVE; + }); + + Services.NetworkMonitor.instance ().network_changed.connect (() => { + if (Services.NetworkMonitor.instance ().network_available) { + foreach (Objects.Source source in Services.Store.instance ().sources) { + source.run_server (); + } + } + }); + }); + var granite_settings = Granite.Settings.get_default (); granite_settings.notify["prefers-color-scheme"].connect (() => { if (Services.Settings.get_default ().settings.get_boolean ("system-appearance")) { @@ -283,54 +348,6 @@ public class MainWindow : Adw.ApplicationWindow { private void init_backend () { Services.Database.get_default ().init_database (); - - if (Services.Store.instance ().is_sources_empty ()) { - Util.get_default ().create_local_source (); - } - - if (Services.Store.instance ().is_database_empty ()) { - Util.get_default ().create_inbox_project (); - Util.get_default ().create_tutorial_project (); - Util.get_default ().create_default_labels (); - } - - sidebar.init (); - - Services.Notification.get_default (); - Services.TimeMonitor.get_default ().init_timeout (); - - go_homepage (); - - Services.Store.instance ().project_deleted.connect (valid_view_removed); - Services.Store.instance ().project_archived.connect (valid_view_removed); - - check_archived (); - - Services.DBusServer.get_default ().item_added.connect ((id) => { - Objects.Item item = Services.Database.get_default ().get_item_by_id (id); - Gee.ArrayList reminders = Services.Database.get_default ().get_reminders_by_item_id (id); - - Services.Store.instance ().add_item (item); - foreach (Objects.Reminder reminder in reminders) { - item.add_reminder_events (reminder); - } - }); - - Timeout.add (Constants.SYNC_TIMEOUT, () => { - foreach (Objects.Source source in Services.Store.instance ().sources) { - source.run_server (); - } - - return GLib.Source.REMOVE; - }); - - Services.NetworkMonitor.instance ().network_changed.connect (() => { - if (Services.NetworkMonitor.instance ().network_available) { - foreach (Objects.Source source in Services.Store.instance ().sources) { - source.run_server (); - } - } - }); } private void check_archived () { @@ -600,4 +617,42 @@ public class MainWindow : Adw.ApplicationWindow { dialog.present (Planify._instance.main_window); } + + private Gtk.Widget build_error_db_page () { + var headerbar = new Adw.HeaderBar (); + + var status_page = new Adw.StatusPage (); + status_page.icon_name = "process-error-symbolic"; + status_page.title = _("Database Integrity Check Failed"); + status_page.description = _("We've detected issues with the database structure that may prevent the application from functioning properly. This may be due to missing tables or columns, likely caused by data corruption or an incomplete update. The database will now be reset to restore normal functionality, and any existing data will be removed. After the reset, you’ll be able to restore any backup you’ve created previously. Thank you for your patience"); + + var reset_button = new Gtk.Button.with_label (_("Reset Database")) { + halign = CENTER + }; + reset_button.add_css_class ("suggested-action"); + + var box = new Gtk.Box (VERTICAL, 12) { + valign = CENTER, + margin_bottom = 32 + }; + box.append (status_page); + box.append (reset_button); + + var toolbar_view = new Adw.ToolbarView () { + content = box + }; + toolbar_view.add_top_bar (headerbar); + + reset_button.clicked.connect (() => { + Services.Database.get_default ().clear_database (); + Services.Settings.get_default ().reset_settings (); + + Timeout.add (250, () => { + init_backend (); + return GLib.Source.REMOVE; + }); + }); + + return toolbar_view; + } }