From 0f565756df99dda186613dc6c516230fed96607f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 01:11:26 -0700 Subject: [PATCH] adds setting screen --- src/application/kordophone-application.vala | 4 + src/application/main-window.vala | 9 ++ src/application/preferences-window.vala | 73 +++++++++++ .../conversation-list-view.vala | 10 ++ src/conversation-list/conversation-row.vala | 7 +- src/meson.build | 6 +- src/models/conversation.vala | 2 +- src/resources/style.css | 5 +- src/service/dbus-service-base.vala | 60 +++++++++ src/service/interface/dbusservice.vala | 3 - .../xml/net.buzzert.kordophonecd.Server.xml | 6 - src/service/repository.vala | 93 +++++--------- src/service/settings.vala | 115 ++++++++++++++++++ 13 files changed, 316 insertions(+), 77 deletions(-) create mode 100644 src/application/preferences-window.vala create mode 100644 src/service/dbus-service-base.vala create mode 100644 src/service/settings.vala diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index 0d5f465..758e4dd 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -20,6 +20,10 @@ public class KordophoneApp : Adw.Application provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); + + // Warm up dbus connections + Settings.get_instance(); + Repository.get_instance(); } protected override void activate () { diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 685c775..ccb44fe 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -24,6 +24,15 @@ public class MainWindow : Adw.ApplicationWindow var transcript_page = new NavigationPage (transcript_view, "Transcript"); split_view.content = transcript_page; + + var show_settings_action = new SimpleAction ("settings", null); + show_settings_action.activate.connect(show_settings); + add_action(show_settings_action); + } + + private void show_settings () { + var dialog = new PreferencesWindow (this); + dialog.present (this); } private void conversation_selected(string? conversation_guid) { diff --git a/src/application/preferences-window.vala b/src/application/preferences-window.vala new file mode 100644 index 0000000..d5f2681 --- /dev/null +++ b/src/application/preferences-window.vala @@ -0,0 +1,73 @@ +using Adw; +using Gtk; + +public class PreferencesWindow : Adw.PreferencesDialog { + private Adw.EntryRow server_url_row; + private Adw.EntryRow username_row; + private Adw.PasswordEntryRow password_row; + private Settings settings; + + public PreferencesWindow (Gtk.Window parent) { + Object ( + title: "Settings" + ); + + add_css_class ("settings-dialog"); + + var page = new PreferencesPage (); + page.margin_top = 14; + page.margin_bottom = 14; + page.margin_start = 50; + page.margin_end = 50; + add (page); + + var connection_group = new PreferencesGroup (); + connection_group.title = "Connection Settings"; + page.add (connection_group); + + server_url_row = new Adw.EntryRow (); + server_url_row.title = "Server URL"; + connection_group.add (server_url_row); + + username_row = new Adw.EntryRow (); + username_row.title = "Username"; + connection_group.add (username_row); + + password_row = new Adw.PasswordEntryRow (); + password_row.title = "Password"; + connection_group.add (password_row); + + settings = Settings.get_instance(); + settings.settings_ready.connect(load_settings); + if (settings.is_connected) { + message("settings is connected"); + load_settings(); + } + } + + private void load_settings() { + try { + username_row.text = settings.get_username(); + server_url_row.text = settings.get_server_url(); + password_row.text = settings.get_password(); + } catch (Error e) { + warning("Failed to load settings: %s", e.message); + } + + setup_change_callbacks(); + } + + private void setup_change_callbacks() { + server_url_row.changed.connect(() => { + settings.set_server_url(server_url_row.text); + }); + + username_row.changed.connect(() => { + settings.set_username(username_row.text); + }); + + password_row.changed.connect(() => { + settings.set_password(password_row.text); + }); + } +} \ No newline at end of file diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 90a0450..cf2fd52 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -48,6 +48,16 @@ public class ConversationListView : Adw.Bin }); header_bar.pack_end (refresh_button); + // Setup application menu + var app_menu = new Menu (); + app_menu.append ("Refresh", "refresh"); + app_menu.append ("Settings...", "win.settings"); + app_menu.append ("Quit", "app.quit"); + + var menu_button = new Gtk.MenuButton (); + menu_button.menu_model = app_menu; + header_bar.pack_end (menu_button); + // Set up model and bind to list conversation_model = new ConversationListModel (); conversation_model.items_changed.connect (on_items_changed); diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index 5ddb121..5f75b80 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -7,8 +7,11 @@ public class ConversationRow : Adw.ActionRow { public ConversationRow(Conversation conversation) { this.conversation = conversation; - title = conversation.display_name; - subtitle = conversation.last_message_preview; + + title = conversation.display_name.strip(); + title_lines = 1; + + subtitle = conversation.last_message_preview.strip(); subtitle_lines = 1; add_css_class("conversation-row"); diff --git a/src/meson.build b/src/meson.build index f143b9d..73c3bdb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,7 +3,8 @@ dependencies = [ dependency('libadwaita-1', required : true), dependency('gio-2.0', required : true), dependency('gee-0.8', required : true), - dependency('gio-unix-2.0', required : true) + dependency('gio-unix-2.0', required : true), + dependency('libsecret-1', required : true), ] gnome = import('gnome') @@ -16,9 +17,12 @@ resources = gnome.compile_resources( sources = [ 'application/kordophone-application.vala', 'application/main-window.vala', + 'application/preferences-window.vala', 'service/interface/dbusservice.vala', + 'service/dbus-service-base.vala', 'service/repository.vala', + 'service/settings.vala', 'conversation-list/conversation-list-view.vala', 'conversation-list/conversation-list-model.vala', diff --git a/src/models/conversation.vala b/src/models/conversation.vala index c7c68a0..563d84f 100644 --- a/src/models/conversation.vala +++ b/src/models/conversation.vala @@ -20,7 +20,7 @@ public class Conversation : Object { } if (participants.length > 1) { - return string.join(", ", participants); + return string.joinv(", ", participants); } return "Untitled"; diff --git a/src/resources/style.css b/src/resources/style.css index f3cadf4..bcad32c 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -28,4 +28,7 @@ .message-input-entry { font-size: 1.1rem; -} \ No newline at end of file +} + + + diff --git a/src/service/dbus-service-base.vala b/src/service/dbus-service-base.vala new file mode 100644 index 0000000..f0a2ffa --- /dev/null +++ b/src/service/dbus-service-base.vala @@ -0,0 +1,60 @@ +public abstract class DBusServiceBase : Object { + protected uint dbus_watch_id; + public bool is_connected { get; private set; default = false; } + + protected const string DBUS_PATH = "/net/buzzert/kordophonecd/daemon"; + protected const string DBUS_NAME = "net.buzzert.kordophonecd"; + + protected DBusServiceBase() { + connect_to_dbus.begin((obj, res) => { + connect_to_dbus.end(res); + }); + } + + ~DBusServiceBase() { + if (dbus_watch_id > 0) { + Bus.unwatch_name(dbus_watch_id); + } + } + + protected abstract void setup_signals(); + protected abstract async Object? get_proxy() throws Error; + protected abstract string get_service_name(); + + private async void connect_to_dbus() { + try { + debug("Trying to connect to %s service at path: %s", get_service_name(), DBUS_PATH); + + var proxy = yield get_proxy(); + if (proxy == null) { + throw new Error(1337, 1, "Failed to get proxy"); + } + + // If we get here, connection succeeded + debug("Connected to %s service at path: %s", get_service_name(), DBUS_PATH); + is_connected = true; + + setup_signals(); + + } catch (Error e) { + debug("Failed to connect to %s at %s: %s", get_service_name(), DBUS_PATH, e.message); + } + + if (!is_connected) { + warning("Failed to connect to %s on any known path", get_service_name()); + + // Watch for the service to appear + dbus_watch_id = Bus.watch_name(BusType.SESSION, + DBUS_NAME, + BusNameWatcherFlags.AUTO_START, + () => { + connect_to_dbus.begin(); + }, + null); + } + } + + protected Error create_not_connected_error() { + return new Error(1337, 1, @"$(get_service_name()) not connected"); + } +} \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index ded23fd..df38113 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -13,9 +13,6 @@ namespace DBusService { [DBus (name = "Username")] public abstract string username { owned get; set; } - [DBus (name = "CredentialItem")] - public abstract GLib.ObjectPath credential_item { owned get; set; } - [DBus (name = "SetServer")] public abstract void set_server(string url, string user) throws DBusError, IOError; diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 01fb1c3..7623714 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -80,12 +80,6 @@ - - - - - diff --git a/src/service/repository.vala b/src/service/repository.vala index f828cf2..f0e5d69 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -1,8 +1,7 @@ using GLib; using Gee; -public class Repository : Object -{ +public class Repository : DBusServiceBase { public signal void conversations_updated(); public signal void messages_updated(string conversation_guid); @@ -15,24 +14,38 @@ public class Repository : Object } private static Repository instance = null; - private DBusService.Repository dbus_repository; - private uint dbus_watch_id; + private DBusService.Repository? dbus_repository; private Repository() { - connect_to_dbus.begin((obj, res) => { - connect_to_dbus.end(res); - }); + base(); } - ~Repository() { - if (dbus_watch_id > 0) { - Bus.unwatch_name(dbus_watch_id); - } + protected override string get_service_name() { + return "Repository"; + } + + protected override async Object? get_proxy() throws Error { + dbus_repository = yield Bus.get_proxy(BusType.SESSION, DBUS_NAME, DBUS_PATH); + dbus_repository.get_version(); // Test the connection + return dbus_repository; + } + + protected override void setup_signals() { + dbus_repository.conversations_updated.connect(() => { + conversations_updated(); + }); + + dbus_repository.messages_updated.connect((conversation_guid) => { + messages_updated(conversation_guid); + }); + + // Initial load + conversations_updated(); } public Conversation[] get_conversations() throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } var conversations = dbus_repository.get_conversations(); @@ -46,8 +59,8 @@ public class Repository : Object } public Message[] get_messages(string conversation_guid, string last_message_id = "") throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } var messages = dbus_repository.get_messages(conversation_guid, last_message_id); @@ -61,56 +74,10 @@ public class Repository : Object } public string send_message(string conversation_guid, string message) throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } return dbus_repository.send_message(conversation_guid, message); } - - private async void connect_to_dbus() { - bool connected = false; - const string path = "/net/buzzert/kordophonecd/daemon"; - - try { - debug("Trying to connect to DBus service at path: %s", path); - dbus_repository = yield Bus.get_proxy(BusType.SESSION, - "net.buzzert.kordophonecd", - path); - - // Test the connection - dbus_repository.get_version(); - - // If we get here, connection succeeded - debug("Connected to DBus service at path: %s", path); - connected = true; - - // Listen for updates - dbus_repository.conversations_updated.connect(() => { - conversations_updated(); - }); - - dbus_repository.messages_updated.connect((conversation_guid) => { - messages_updated(conversation_guid); - }); - - // Initial load - conversations_updated(); - } catch (Error e) { - debug("Failed to connect to kordophonecd at %s: %s", path, e.message); - } - - if (!connected) { - warning("Failed to connect to kordophonecd on any known path"); - - // Watch for the service to appear - dbus_watch_id = Bus.watch_name(BusType.SESSION, - "net.buzzert.kordophonecd", - BusNameWatcherFlags.AUTO_START, - () => { - connect_to_dbus.begin(); - }, - null); - } - } } diff --git a/src/service/settings.vala b/src/service/settings.vala new file mode 100644 index 0000000..07adfd0 --- /dev/null +++ b/src/service/settings.vala @@ -0,0 +1,115 @@ +using GLib; + +public class Settings : DBusServiceBase { + public signal void config_changed(); + public signal void settings_ready(); + + public static Settings get_instance() { + if (instance == null) { + instance = new Settings(); + } + return instance; + } + + private static Settings instance = null; + private DBusService.Settings? dbus_settings; + private Secret.Service secret_service; + + private Settings() { + base(); + + try { + secret_service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION); + } catch (Error e) { + warning("Failed to get secret service: %s", e.message); + } + } + + protected override string get_service_name() { + return "Settings"; + } + + protected override async Object? get_proxy() throws Error { + dbus_settings = yield Bus.get_proxy(BusType.SESSION, DBUS_NAME, DBUS_PATH); + return dbus_settings; + } + + protected override void setup_signals() { + dbus_settings.config_changed.connect(() => { + config_changed(); + }); + + settings_ready(); + } + + public string get_server_url() throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + return dbus_settings.server_u_r_l; + } + + public void set_server_url(string url) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.server_u_r_l = url; + } + + public string get_username() throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + return dbus_settings.username; + } + + public void set_username(string username) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.username = username; + } + + public void set_server(string url, string username) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.set_server(url, username); + } + + private HashTable password_attributes() { + var attributes = new HashTable(str_hash, str_equal); + attributes["service"] = "net.buzzert.kordophonecd"; + attributes["target"] = "default"; + attributes["username"] = get_username(); + return attributes; + } + + public string get_password() throws Error { + var attributes = password_attributes(); + var password = secret_service.lookup_sync(null, attributes, null); + if (password == null) { + warning("No password found for user %s", get_username()); + return ""; + } + + return password.get_text(); + } + + public void set_password(string password) throws Error { + var attributes = password_attributes(); + bool result = secret_service.store_sync( + null, + attributes, + Secret.COLLECTION_DEFAULT, + "Kordophone Keystore", + new Secret.Value(password, password.length, "text/plain"), + null + ); + + if (!result) { + warning("Failed to store password for user %s", get_username()); + throw new Error(1337, 1, "Failed to store password"); + } + } +} \ No newline at end of file