Changes
6 changed files (+334/-25)
-
-
@@ -41,6 +41,8 @@ };const gresources = [_][]const u8{ "data/ui/server-list.ui", "data/ui/server-list-unexpected-error-dialog.ui", "data/ui/server-list-network-error-dialog.ui", }; // Vala source codes to compile. As Vala does not have module system,
-
-
-
@@ -19,5 +19,7 @@ --><gresources> <gresource prefix="/jp/pocka/plac/gtk-adwaita"> <file preprocess="xml-stripblanks">ui/server-list.ui</file> <file preprocess="xml-stripblanks">ui/server-list-unexpected-error-dialog.ui</file> <file preprocess="xml-stripblanks">ui/server-list-network-error-dialog.ui</file> </gresource> </gresources>
-
-
-
@@ -0,0 +1,66 @@<?xml version="1.0" encoding="utf-8"?> <!-- 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 --> <interface> <template class="PlacServerListNetworkErrorDialog" parent="AdwDialog"> <property name="follows-content-size">true</property> <property name="title">Failed to scan Roon Servers</property> <child> <object class="AdwToolbarView"> <child type="top"> <object class="AdwHeaderBar" /> </child> <property name="content"> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">4</property> <property name="margin-top">4</property> <property name="margin-bottom">16</property> <property name="margin-start">12</property> <property name="margin-end">12</property> <child> <object class="GtkLabel"> <property name="wrap">true</property> <property name="label">Unable to scan Roon Servers due to a network error. Check your network connection and if this program has network access permission.</property> <property name="halign">start</property> </object> </child> <child> <object class="GtkLabel"> <property name="label">Details</property> <property name="margin-top">12</property> <property name="halign">start</property> <style> <class name="heading" /> </style> </object> </child> <child> <object class="GtkLabel" id="details_label"> <property name="wrap">true</property> <property name="halign">start</property> </object> </child> </object> </property> </object> </child> </template> </interface>
-
-
-
@@ -0,0 +1,64 @@<?xml version="1.0" encoding="utf-8"?> <!-- 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 --> <interface> <template class="PlacServerListUnexpectedErrorDialog" parent="AdwDialog"> <property name="follows-content-size">true</property> <property name="title">Failed to scan Roon Servers</property> <child> <object class="AdwToolbarView"> <child type="top"> <object class="AdwHeaderBar" /> </child> <property name="content"> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">4</property> <property name="margin-top">4</property> <property name="margin-bottom">16</property> <property name="margin-start">12</property> <property name="margin-end">12</property> <child> <object class="GtkLabel"> <property name="wrap">true</property> <property name="label">Encountered an unexpected error during scan operation. Scan again to see if the problem resolves.</property> <property name="halign">start</property> </object> </child> <child> <object class="GtkLabel"> <property name="label">Details</property> <property name="margin-top">12</property> <property name="halign">start</property> <style> <class name="heading" /> </style> </object> </child> <child> <object class="GtkLabel" id="details_label"> <property name="wrap">true</property> <property name="halign">start</property> </object> </child> </object> </property> </object> </child> </template> </interface>
-
-
-
@@ -20,28 +20,72 @@ <interface><template class="PlacServerSelectorWindow" parent="AdwApplicationWindow"> <property name="default-width">300</property> <property name="default-height">300</property> <property name="title">Server Selector</property> <property name="title">Select Roon Server</property> <property name="content"> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">8</property> <child> <object class="GtkLabel"> <property name="label">Select Roon Server to connect.</property> <attributes> <attribute name="weight" value="PANGO_WEIGHT_BOLD" /> </attributes> <object class="AdwToolbarView"> <child type="top"> <object class="AdwHeaderBar"> <child type="start"> <object class="GtkButton" id="scan_button"> <property name="label">Scan</property> <property name="action-name">win.scan_servers</property> </object> </child> </object> </child> <child> <object class="GtkListBox" id="servers_list"> <child type="placeholder"> <object class="GtkLabel"> <property name="label">Scanning Roon Servers on network...</property> <property name="content"> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">8</property> <child> <object class="AdwBanner" id="failure_banner"> <property name="title">Failed to scan Roon Servers</property> <property name="button-label">Show Details</property> <style> <class name="error" /> </style> </object> </child> <child> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">2</property> <child> <object class="GtkBox" id="statusbar_loading"> <property name="visible">false</property> <property name="orientation">horizontal</property> <property name="spacing">2</property> <child> <object class="AdwSpinner" /> </child> <child> <object class="GtkLabel"> <property name="label">Scanning Roon Server on local network...</property> </object> </child> </object> </child> </object> </child> <child> <object class="AdwClamp"> <child> <object class="GtkListBox" id="servers_list"> <property name="selection-mode">none</property> </object> </child> </object> </child> <child> <object class="AdwStatusPage" id="empty"> <property name="visible">false</property> <property name="title">No Servers Found</property> <property name="description">Scan again after setting up Roon Server.</property> <property name="icon-name">arrow-into-box</property> </object> </child> </object> </child> </property> </object> </property> </template>
-
-
-
@@ -33,35 +33,111 @@ return new App().run(args);} } public errordomain ScanError { 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.Box statusbar_loading; [GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Adw.StatusPage empty; private ulong error_detail_hid; public ServerSelectorWindow(Gtk.Application app) { Object(application: app); } scan_servers.begin((_obj, res) => { var servers = scan_servers.end(res); construct { scan_servers(); var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(this.scan_servers); this.add_action(scan_action); } private void scan_servers() { failure_banner.revealed = false; if (error_detail_hid > 0) { failure_banner.disconnect(error_detail_hid); error_detail_hid = 0; } statusbar_loading.visible = true; scan_button.set_sensitive(false); scan_servers_async.begin((_obj, res) => { try { var servers = scan_servers_async.end(res); servers_list.remove_all(); if (servers.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 Plac.Server server in servers) { var row = new Adw.ActionRow(); row.title = server.name; row.subtitle = server.version; foreach (unowned Plac.Server server in servers) { var row = new Adw.ActionRow(); row.title = server.name; row.subtitle = server.version; servers_list.append(row); } } catch (ScanError e) { // Have to capture, closure can't access `e` here (why? idk). var message = e.message; var is_network_error = e is ScanError.NETWORK_ERROR; servers_list.append(row); scan_button.add_css_class("suggested-action"); this.error_detail_hid = failure_banner.button_clicked.connect(() => { if (is_network_error) { var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); } else { var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); } }); failure_banner.revealed = true; } finally { statusbar_loading.visible = false; scan_button.set_sensitive(true); } }); } private async Plac.Server[] scan_servers() { SourceFunc callback = scan_servers.callback; private async Plac.Server[] scan_servers_async() throws ScanError { SourceFunc callback = scan_servers_async.callback; Plac.Server[] servers = {}; Plac.ScanResultCode code = Plac.ScanResultCode.UNKNOWN_ERROR; ThreadFunc<void>run = () => { var opts = Plac.ScanOptions(); opts.count = 4; opts.receive_window_ms = 1300; var scanner = new Plac.ServerScanner(); var result = scanner.scan(opts); code = result.code; while (true) { var next = result.next();
-
@@ -77,7 +153,62 @@new Thread<void>("server_scanner", (owned) run); yield; switch (code) { case Plac.ScanResultCode.OK: break; case Plac.ScanResultCode.NETWORK_UNAVAILABLE: throw new ScanError.NETWORK_ERROR("Network unavailable"); case Plac.ScanResultCode.SOCKET_SETUP_ERROR: case Plac.ScanResultCode.TOO_MANY_SOCKET_ERROR: throw new ScanError.NETWORK_ERROR("Unable to setup UDP socket"); case Plac.ScanResultCode.SOCKET_PERMISSION_ERROR: throw new ScanError.NETWORK_ERROR("No permission to open UDP socket"); case Plac.ScanResultCode.UDP_RECV_ERROR: throw new ScanError.NETWORK_ERROR("Unable to receive UDP packet"); case Plac.ScanResultCode.UDP_SEND_ERROR: throw new ScanError.NETWORK_ERROR("Unable to send UDP packet"); case Plac.ScanResultCode.OUT_OF_MEMORY: throw new ScanError.UNEXPECTED_ERROR("Out of memory"); case Plac.ScanResultCode.UNKNOWN_ERROR: case Plac.ScanResultCode.INVALID_RECEIVE_WINDOW: case Plac.ScanResultCode.NULL_POINTER_ARGS: default: throw new ScanError.UNEXPECTED_ERROR("Unexpected error"); } return (owned) servers; } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-unexpected-error-dialog.ui")] class ServerListUnexpectedErrorDialog : Adw.Dialog { [GtkChild] private unowned Gtk.Label details_label; public string details { get; construct; } public ServerListUnexpectedErrorDialog(string details) { Object(details: details); } construct { details_label.label = details; } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-network-error-dialog.ui")] class ServerListNetworkErrorDialog : Adw.Dialog { [GtkChild] private unowned Gtk.Label details_label; public string details { get; construct; } public ServerListNetworkErrorDialog(string details) { Object(details: details); } construct { details_label.label = details; } } }
-