Changes
4 changed files (+324/-273)
-
-
@@ -36,224 +36,6 @@ return new App().run(args);} } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/generic-error-dialog.ui")] class GenericErrorDialog : Adw.Dialog { [GtkChild] private unowned Gtk.Label description_label; [GtkChild] private unowned Gtk.Label details_label; public string description { get { return description_label.label; } set { description_label.label = value; } } public string details { get { return details_label.label; } set { details_label.label = value; } } public GenericErrorDialog() { Object(); } } private enum ScanErrorKind { UNEXPECTED_ERROR, NETWORK_ERROR, } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list.ui")] class ServerSelectorWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.ListBox servers_list; [GtkChild] private unowned Adw.Banner failure_banner; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Adw.StatusPage empty; private ulong error_detail_hid; private unowned PlacCore.App core; public ServerSelectorWindow(Gtk.Application app, PlacCore.App core) { Object(application: app); this.core = core; } private string? get_and_create_state_dir() { var state_dir = GLib.Environment.get_user_state_dir(); var dir = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, state_dir, application.application_id); var file = GLib.File.new_for_path(dir); if (!file.query_exists()) { try { file.make_directory(); } catch (Error e) { stderr.printf("Unable to create state directory: %s\n", e.message); return null; } } return dir; } private void set_state_file() { var dir = get_and_create_state_dir(); if (dir == null) { return; } var state_path = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, dir, "state.json"); core.set_state_path(state_path, state_path.length); } public void start() { this.set_state_file(); core.on_restore_complete(() => { if (core.connection == PlacCore.ConnectionState.IDLE) { this.start_scanning(); return; } else if (core.connection == PlacCore.ConnectionState.BUSY) { GLib.Idle.add(() => { this.open_restored_window(); return false; }); } }); core.restore_state(); var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(core.server_selector.load); this.add_action(scan_action); this.present(); } private void open_restored_window() { var window = new MainWindow(application, core); window.start(); this.close(); return; } private void start_scanning() { core.server_selector.on_change(() => { GLib.Idle.add(() => { core.server_selector.lock(); this.render(); core.server_selector.unlock(); return false; }); }); core.server_selector.load(); core.server_selector.lock(); this.render(); core.server_selector.unlock(); } private void render() { switch (core.server_selector.state) { case PlacCore.ServerSelectorState.REFRESHING: case PlacCore.ServerSelectorState.LOADING: failure_banner.revealed = false; if (error_detail_hid > 0) { failure_banner.disconnect(error_detail_hid); error_detail_hid = 0; } stack.visible_child_name = "loading"; scan_button.set_sensitive(false); break; case PlacCore.ServerSelectorState.NOT_LOADED: break; case PlacCore.ServerSelectorState.LOADED: servers_list.remove_all(); if (core.server_selector.entries.length == 0) { servers_list.visible = false; empty.visible = true; scan_button.add_css_class("suggested-action"); } else { servers_list.visible = true; empty.visible = false; scan_button.remove_css_class("suggested-action"); } foreach (unowned PlacCore.ServerSelectorEntry entry in core.server_selector.entries) { var row = new Adw.ActionRow(); row.title = entry.name; row.subtitle = entry.version; var main_window = new MainWindow(this.application, core); row.activatable_widget = main_window; row.activated.connect(() => { main_window.start(); core.connect(entry.id, entry.id.length, null, 0); this.close(); }); servers_list.append(row); } stack.visible_child_name = "idle"; scan_button.set_sensitive(true); break; case PlacCore.ServerSelectorState.ERR_UNEXPECTED: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unexpected error"); break; case PlacCore.ServerSelectorState.ERR_NETWORK_UNAVAILABLE: show_error(ScanErrorKind.NETWORK_ERROR, "Network unavailable"); break; case PlacCore.ServerSelectorState.ERR_SOCKET_PERMISSION: show_error(ScanErrorKind.NETWORK_ERROR, "No permission to create UDP socket"); break; case PlacCore.ServerSelectorState.ERR_OUT_OF_MEMORY: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Out of memory"); break; case PlacCore.ServerSelectorState.ERR_SOCKET: show_error(ScanErrorKind.NETWORK_ERROR, "Failed to operate on a socket"); break; case PlacCore.ServerSelectorState.ERR_THREAD_SPAWN: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unable to spawn a thread"); break; } } private void show_error(ScanErrorKind kind, string message) { scan_button.add_css_class("suggested-action"); this.error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case ScanErrorKind.NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); break; case ScanErrorKind.UNEXPECTED_ERROR: var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); break; } }); failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.set_sensitive(true); } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-unexpected-error-dialog.ui")] class ServerListUnexpectedErrorDialog : GenericErrorDialog { public ServerListUnexpectedErrorDialog(string details) {
-
@@ -272,61 +54,6 @@ [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-connecting.ui")]class ServerConnecting : Gtk.Box { public ServerConnecting() { Object(); } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/main-window.ui")] class MainWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.Label test_label; [GtkChild] private unowned Gtk.Stack root_stack; private unowned PlacCore.App core; public MainWindow(Gtk.Application app, PlacCore.App core) { (typeof (ServerConnecting)).ensure(); Object(application: app); this.core = core; } public void start() { core.on_connection_change(() => { GLib.Idle.add(() => { core.lock(); this.render(); core.unlock(); return false; }); }); this.present(); core.lock(); this.render(); core.unlock(); } private void render() { switch (core.connection) { case PlacCore.ConnectionState.IDLE: if (core.server == null) { return; } root_stack.visible_child_name = "main"; test_label.label = core.server.name; break; case PlacCore.ConnectionState.BUSY: root_stack.visible_child_name = "loading"; break; default: test_label.label = core.connection.to_string(); break; } } } }
-
-
-
@@ -0,0 +1,39 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/generic-error-dialog.ui")] class GenericErrorDialog : Adw.Dialog { [GtkChild] private unowned Gtk.Label description_label; [GtkChild] private unowned Gtk.Label details_label; public string description { get { return description_label.label; } set { description_label.label = value; } } public string details { get { return details_label.label; } set { details_label.label = value; } } public GenericErrorDialog() { Object(); } } }
-
-
-
@@ -0,0 +1,72 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/main-window.ui")] class MainWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.Label test_label; [GtkChild] private unowned Gtk.Stack root_stack; private unowned PlacCore.App core; public MainWindow(Gtk.Application app, PlacCore.App core) { (typeof (ServerConnecting)).ensure(); Object(application: app); this.core = core; } public void start() { core.on_connection_change(() => { GLib.Idle.add(() => { core.lock(); this.render(); core.unlock(); return false; }); }); this.present(); core.lock(); this.render(); core.unlock(); } private void render() { switch (core.connection) { case PlacCore.ConnectionState.IDLE: if (core.server == null) { return; } root_stack.visible_child_name = "main"; test_label.label = core.server.name; break; case PlacCore.ConnectionState.BUSY: root_stack.visible_child_name = "loading"; break; default: test_label.label = core.connection.to_string(); break; } } } }
-
-
-
@@ -0,0 +1,213 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list.ui")] class ServerSelectorWindow : Adw.ApplicationWindow { private enum ScanErrorKind { UNEXPECTED_ERROR, NETWORK_ERROR, } [GtkChild] private unowned Gtk.ListBox servers_list; [GtkChild] private unowned Adw.Banner failure_banner; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Adw.StatusPage empty; private ulong error_detail_hid; private unowned PlacCore.App core; public ServerSelectorWindow(Gtk.Application app, PlacCore.App core) { Object(application: app); this.core = core; } private string? get_and_create_state_dir() { var state_dir = GLib.Environment.get_user_state_dir(); var dir = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, state_dir, application.application_id); var file = GLib.File.new_for_path(dir); if (!file.query_exists()) { try { file.make_directory(); } catch (Error e) { stderr.printf("Unable to create state directory: %s\n", e.message); return null; } } return dir; } private void set_state_file() { var dir = get_and_create_state_dir(); if (dir == null) { return; } var state_path = GLib.Path.build_path(GLib.Path.DIR_SEPARATOR_S, dir, "state.json"); core.set_state_path(state_path, state_path.length); } public void start() { this.set_state_file(); core.on_restore_complete(() => { if (core.connection == PlacCore.ConnectionState.IDLE) { this.start_scanning(); return; } else if (core.connection == PlacCore.ConnectionState.BUSY) { GLib.Idle.add(() => { this.open_restored_window(); return false; }); } }); core.restore_state(); var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(core.server_selector.load); this.add_action(scan_action); this.present(); } private void open_restored_window() { var window = new MainWindow(application, core); window.start(); this.close(); return; } private void start_scanning() { core.server_selector.on_change(() => { GLib.Idle.add(() => { core.server_selector.lock(); this.render(); core.server_selector.unlock(); return false; }); }); core.server_selector.load(); core.server_selector.lock(); this.render(); core.server_selector.unlock(); } private void render() { switch (core.server_selector.state) { case PlacCore.ServerSelectorState.REFRESHING: case PlacCore.ServerSelectorState.LOADING: failure_banner.revealed = false; if (error_detail_hid > 0) { failure_banner.disconnect(error_detail_hid); error_detail_hid = 0; } stack.visible_child_name = "loading"; scan_button.set_sensitive(false); break; case PlacCore.ServerSelectorState.NOT_LOADED: break; case PlacCore.ServerSelectorState.LOADED: servers_list.remove_all(); if (core.server_selector.entries.length == 0) { servers_list.visible = false; empty.visible = true; scan_button.add_css_class("suggested-action"); } else { servers_list.visible = true; empty.visible = false; scan_button.remove_css_class("suggested-action"); } foreach (unowned PlacCore.ServerSelectorEntry entry in core.server_selector.entries) { var row = new Adw.ActionRow(); row.title = entry.name; row.subtitle = entry.version; var main_window = new MainWindow(this.application, core); row.activatable_widget = main_window; row.activated.connect(() => { main_window.start(); core.connect(entry.id, entry.id.length, null, 0); this.close(); }); servers_list.append(row); } stack.visible_child_name = "idle"; scan_button.set_sensitive(true); break; case PlacCore.ServerSelectorState.ERR_UNEXPECTED: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unexpected error"); break; case PlacCore.ServerSelectorState.ERR_NETWORK_UNAVAILABLE: show_error(ScanErrorKind.NETWORK_ERROR, "Network unavailable"); break; case PlacCore.ServerSelectorState.ERR_SOCKET_PERMISSION: show_error(ScanErrorKind.NETWORK_ERROR, "No permission to create UDP socket"); break; case PlacCore.ServerSelectorState.ERR_OUT_OF_MEMORY: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Out of memory"); break; case PlacCore.ServerSelectorState.ERR_SOCKET: show_error(ScanErrorKind.NETWORK_ERROR, "Failed to operate on a socket"); break; case PlacCore.ServerSelectorState.ERR_THREAD_SPAWN: show_error(ScanErrorKind.UNEXPECTED_ERROR, "Unable to spawn a thread"); break; } } private void show_error(ScanErrorKind kind, string message) { scan_button.add_css_class("suggested-action"); this.error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case ScanErrorKind.NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); break; case ScanErrorKind.UNEXPECTED_ERROR: var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); break; } }); failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.set_sensitive(true); } } }
-