Changes
14 changed files (+1083/-1084)
-
-
@@ -15,129 +15,129 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { public class App : Adw.Application { private static GLib.ActionEntry[] app_entries = { GLib.ActionEntry() { name = "preferences", activate = on_preferences, }, GLib.ActionEntry() { name = "about", activate = on_about, }, GLib.ActionEntry() { name = "quit", activate = on_quit, }, }; public class App : Adw.Application { private static GLib.ActionEntry[] app_entries = { GLib.ActionEntry() { name = "preferences", activate = on_preferences, }, GLib.ActionEntry() { name = "about", activate = on_about, }, GLib.ActionEntry() { name = "quit", activate = on_quit, }, }; private Settings settings = new Settings(); private Settings settings = new Settings(); public App(){ Object( application_id: "jp.pocka.plac.gtk-adwaita", flags : ApplicationFlags.DEFAULT_FLAGS ); } public App(){ Object( application_id: "jp.pocka.plac.gtk-adwaita", flags : ApplicationFlags.DEFAULT_FLAGS ); } protected override void activate() { if (settings.connected_server_id == "") { var selector = new ServerSelector.Window(this); selector.start(); } else { var main_window = new MainWindow.from_server_id(this, settings.connected_server_id); if (settings.connected_server_addr != "" && settings.connected_server_port > 0) { main_window.start_with_addr( settings.connected_server_addr, settings.connected_server_port ); protected override void activate() { if (settings.connected_server_id == "") { var selector = new ServerSelector.Window(this); selector.start(); } else { main_window.start(); var main_window = new MainWindow.from_server_id(this, settings.connected_server_id); if (settings.connected_server_addr != "" && settings.connected_server_port > 0) { main_window.start_with_addr( settings.connected_server_addr, settings.connected_server_port ); } else { main_window.start(); } } } } public static int main(string[] args) { return new App().run(args); } public static int main(string[] args) { return new App().run(args); } private void on_preferences() { var label_parsing_enabled = new Adw.SwitchRow(); settings.settings.bind(Settings.LABEL_PARSING_ENABLED, label_parsing_enabled, "active", DEFAULT); label_parsing_enabled.title = "Parse labels"; label_parsing_enabled.subtitle = "Enable parsing of \"[[id|text]]\" labels in browse section."; private void on_preferences() { var label_parsing_enabled = new Adw.SwitchRow(); settings.settings.bind(Settings.LABEL_PARSING_ENABLED, label_parsing_enabled, "active", DEFAULT); label_parsing_enabled.title = "Parse labels"; label_parsing_enabled.subtitle = "Enable parsing of \"[[id|text]]\" labels in browse section."; var show_browse_item_separators = new Adw.SwitchRow(); settings.settings.bind(Settings.SHOW_BROWSE_ITEM_SEPARATORS, show_browse_item_separators, "active", DEFAULT); show_browse_item_separators.title = "Show separators for browse items"; show_browse_item_separators.subtitle = "Show separator between browse items."; var show_browse_item_separators = new Adw.SwitchRow(); settings.settings.bind(Settings.SHOW_BROWSE_ITEM_SEPARATORS, show_browse_item_separators, "active", DEFAULT); show_browse_item_separators.title = "Show separators for browse items"; show_browse_item_separators.subtitle = "Show separator between browse items."; var browse = new Adw.PreferencesGroup(); browse.add(label_parsing_enabled); browse.add(show_browse_item_separators); browse.title = "Browse"; var browse = new Adw.PreferencesGroup(); browse.add(label_parsing_enabled); browse.add(show_browse_item_separators); browse.title = "Browse"; var show_seek_by_10secs = new Adw.SwitchRow(); settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, show_seek_by_10secs, "active", DEFAULT); show_seek_by_10secs.title = "Show seek by 10secs buttons"; show_seek_by_10secs.subtitle = "Show buttons that seek forward/backwards by 10 seconds."; var show_seek_by_10secs = new Adw.SwitchRow(); settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, show_seek_by_10secs, "active", DEFAULT); show_seek_by_10secs.title = "Show seek by 10secs buttons"; show_seek_by_10secs.subtitle = "Show buttons that seek forward/backwards by 10 seconds."; var playback = new Adw.PreferencesGroup(); playback.add(show_seek_by_10secs); playback.title = "Playback"; var playback = new Adw.PreferencesGroup(); playback.add(show_seek_by_10secs); playback.title = "Playback"; var page = new Adw.PreferencesPage(); page.add(browse); page.add(playback); var page = new Adw.PreferencesPage(); page.add(browse); page.add(playback); var dialog = new Adw.PreferencesDialog(); dialog.add(page); dialog.present(this.active_window); } var dialog = new Adw.PreferencesDialog(); dialog.add(page); dialog.present(this.active_window); } private void on_about() { var dialog = new Adw.AboutDialog.from_appdata("/jp/pocka/plac/gtk-adwaita/metainfo.xml", null); private void on_about() { var dialog = new Adw.AboutDialog.from_appdata("/jp/pocka/plac/gtk-adwaita/metainfo.xml", null); // TODO: Inject these via build parameter (JSON or REUSE or whatever) dialog.copyright = "Copyright 2025 Shota FUJI"; dialog.version = "0.0.0"; // TODO: Inject these via build parameter (JSON or REUSE or whatever) dialog.copyright = "Copyright 2025 Shota FUJI"; dialog.version = "0.0.0"; dialog.add_legal_section("libsood", "Copyright 2025 Shota FUJI", APACHE_2_0, null); dialog.add_legal_section("libmoo", "Copyright 2025 Shota FUJI", APACHE_2_0, null); dialog.add_legal_section("websocket.zig", "Copyright (c) 2024 Karl Seguin.", MIT_X11, null); dialog.add_legal_section("libsood", "Copyright 2025 Shota FUJI", APACHE_2_0, null); dialog.add_legal_section("libmoo", "Copyright 2025 Shota FUJI", APACHE_2_0, null); dialog.add_legal_section("websocket.zig", "Copyright (c) 2024 Karl Seguin.", MIT_X11, null); dialog.present(this.active_window); } dialog.present(this.active_window); } private void on_quit() { this.quit(); } private void on_quit() { this.quit(); } public override void startup() { base.startup(); public override void startup() { base.startup(); this.add_action_entries(app_entries, this); this.add_action_entries(app_entries, this); } } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-unexpected-error-dialog.ui")] class ServerListUnexpectedErrorDialog : GenericErrorDialog { public ServerListUnexpectedErrorDialog(string details) { Object(details: details); [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-unexpected-error-dialog.ui")] class ServerListUnexpectedErrorDialog : GenericErrorDialog { public ServerListUnexpectedErrorDialog(string details) { Object(details: details); } } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-network-error-dialog.ui")] class ServerListNetworkErrorDialog : GenericErrorDialog { public ServerListNetworkErrorDialog(string details) { Object(details: details); [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-network-error-dialog.ui")] class ServerListNetworkErrorDialog : GenericErrorDialog { public ServerListNetworkErrorDialog(string details) { Object(details: details); } } } [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/server-connecting.ui")] class ServerConnecting : Gtk.Box { public ServerConnecting() { Object(); } } } }
-
-
-
@@ -15,75 +15,75 @@ //// SPDX-License-Identifier: Apache-2.0 namespace Plac { namespace Discovery { public async Plac.Discovery.ScanResult? scan_async() { GLib.SourceFunc callback = scan_async.callback; Plac.Discovery.ScanResult? result = null; namespace Discovery { public async Plac.Discovery.ScanResult? scan_async() { GLib.SourceFunc callback = scan_async.callback; Plac.Discovery.ScanResult? result = null; new GLib.Thread<void>("server-scanner", () => { new GLib.Thread<void>("server-scanner", () => { result = Plac.Discovery.scan(); GLib.Idle.add((owned) callback); }); yield; return (owned) result; } yield; return (owned) result; } public async Plac.Discovery.ScanResult? find_async(string server_id) { GLib.SourceFunc callback = find_async.callback; Plac.Discovery.ScanResult? result = null; public async Plac.Discovery.ScanResult? find_async(string server_id) { GLib.SourceFunc callback = find_async.callback; Plac.Discovery.ScanResult? result = null; new GLib.Thread<void>("server-find", () => { new GLib.Thread<void>("server-find", () => { result = Plac.Discovery.find(server_id); GLib.Idle.add((owned) callback); }); yield; return (owned) result; } yield; return (owned) result; } public async Plac.Discovery.ScanResult? resolve_async(string server_id, string ip_addr, uint16 http_port) { GLib.SourceFunc callback = resolve_async.callback; Plac.Discovery.ScanResult? result = null; public async Plac.Discovery.ScanResult? resolve_async(string server_id, string ip_addr, uint16 http_port) { GLib.SourceFunc callback = resolve_async.callback; Plac.Discovery.ScanResult? result = null; new GLib.Thread<void>("server-resolve", () => { new GLib.Thread<void>("server-resolve", () => { result = Plac.Discovery.resolve(server_id, ip_addr, http_port); GLib.Idle.add((owned) callback); }); yield; return (owned) result; } } yield; return (owned) result; } } public class AsyncConnection : GLib.Object { private Connection conn; private GLib.Thread<void>? thread = null; private bool is_closed = false; public size_t retry_max = 3; public class AsyncConnection : GLib.Object { private Connection conn; private GLib.Thread<void>? thread = null; private bool is_closed = false; public size_t retry_max = 3; public AsyncConnection(Discovery.Server server) { this.conn = new Connection(server, null); } public AsyncConnection(Discovery.Server server) { this.conn = new Connection(server, null); } public AsyncConnection.with_token(Discovery.Server server, string token) { this.conn = new Connection(server, token); } public AsyncConnection.with_token(Discovery.Server server, string token) { this.conn = new Connection(server, token); } public signal void connection_started(); public signal void out_of_memory_error(); public signal void connection_error(ConnectionErrorEvent event); public signal void connected(ConnectedEvent event); public signal void zones_changed(Transport.ZoneListEvent event); public signal void connection_started(); public signal void out_of_memory_error(); public signal void connection_error(ConnectionErrorEvent event); public signal void connected(ConnectedEvent event); public signal void zones_changed(Transport.ZoneListEvent event); public void activate() { if (thread != null) { return; } public void activate() { if (thread != null) { return; } is_closed = false; is_closed = false; thread = new GLib.Thread<void>("connection-loop", () => { thread = new GLib.Thread<void>("connection-loop", () => { GLib.Idle.add(() => { connection_started(); return false;
-
@@ -144,102 +144,102 @@ }} } }); } public void deactivate() { if (thread == null) { return; } // Prevent unnecessary read immediately. is_closed = true; public void deactivate() { if (thread == null) { return; } // Schedule thread disposal. Calling `thread.join` immediately results in // `join` from the same thread = deadlock. GLib.Idle.add(() => { // Prevent unnecessary read immediately. is_closed = true; // Schedule thread disposal. Calling `thread.join` immediately results in // `join` from the same thread = deadlock. GLib.Idle.add(() => { thread.join(); return false; }); } } public async void control(Transport.Zone zone, uint16 action) { GLib.SourceFunc callback = control.callback; public async void control(Transport.Zone zone, uint16 action) { GLib.SourceFunc callback = control.callback; new GLib.Thread<void>("control", () => { new GLib.Thread<void>("control", () => { conn.control(zone, action); GLib.Idle.add((owned) callback); }); yield; } yield; } public async Transport.SeekResultCode seek(Transport.Zone zone, int64 at_seconds) { GLib.SourceFunc callback = seek.callback; Transport.SeekResultCode code = UNKNOWN_ERROR; public async Transport.SeekResultCode seek(Transport.Zone zone, int64 at_seconds) { GLib.SourceFunc callback = seek.callback; Transport.SeekResultCode code = UNKNOWN_ERROR; new GLib.Thread<void>("seek", () => { new GLib.Thread<void>("seek", () => { code = conn.seek(zone, at_seconds); GLib.Idle.add((owned) callback); }); yield; return code; } yield; return code; } public async Transport.VolumeControlResultCode increase_volume(Transport.Output output) { GLib.SourceFunc callback = increase_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; public async Transport.VolumeControlResultCode increase_volume(Transport.Output output) { GLib.SourceFunc callback = increase_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; new GLib.Thread<void>("increase_volume", () => { new GLib.Thread<void>("increase_volume", () => { code = conn.increase_volume(output); GLib.Idle.add((owned) callback); }); yield; return code; } yield; return code; } public async Transport.VolumeControlResultCode decrease_volume(Transport.Output output) { GLib.SourceFunc callback = decrease_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; public async Transport.VolumeControlResultCode decrease_volume(Transport.Output output) { GLib.SourceFunc callback = decrease_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; new GLib.Thread<void>("decrease_volume", () => { new GLib.Thread<void>("decrease_volume", () => { code = conn.decrease_volume(output); GLib.Idle.add((owned) callback); }); yield; return code; } yield; return code; } public async Transport.VolumeControlResultCode change_volume(Transport.Output output, double value) { GLib.SourceFunc callback = change_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; public async Transport.VolumeControlResultCode change_volume(Transport.Output output, double value) { GLib.SourceFunc callback = change_volume.callback; Transport.VolumeControlResultCode code = UNKNOWN_ERROR; new GLib.Thread<void>("change_volume", () => { new GLib.Thread<void>("change_volume", () => { code = conn.change_volume(output, value); GLib.Idle.add((owned) callback); }); yield; return code; } yield; return code; } public async Browse.Result? browse(Browse.Hierarchy hierarchy, Transport.Zone? zone, Browse.Item? item, bool back) { GLib.SourceFunc callback = browse.callback; Browse.Result? result = null; public async Browse.Result? browse(Browse.Hierarchy hierarchy, Transport.Zone? zone, Browse.Item? item, bool back) { GLib.SourceFunc callback = browse.callback; Browse.Result? result = null; new GLib.Thread<void>("browse", () => { new GLib.Thread<void>("browse", () => { result = conn.browse(hierarchy, zone, item, back); GLib.Idle.add((owned) callback); }); yield; return (owned) result; } yield; return (owned) result; } public string? get_image_url(string image_key, Image.GetOptions options) { return conn.get_image_url(image_key, options); public string? get_image_url(string image_key, Image.GetOptions options) { return conn.get_image_url(image_key, options); } } } }
-
-
-
@@ -15,45 +15,45 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { private const string CONNECTED_SERVER_ID = "connected-server-id"; private const string CONNECTED_SERVER_TOKEN = "connected-server-token"; private const string CONNECTED_SERVER_ADDR = "connected-server-addr"; private const string CONNECTED_SERVER_PORT = "connected-server-port"; private const string CONNECTED_SERVER_ID = "connected-server-id"; private const string CONNECTED_SERVER_TOKEN = "connected-server-token"; private const string CONNECTED_SERVER_ADDR = "connected-server-addr"; private const string CONNECTED_SERVER_PORT = "connected-server-port"; public class Settings { public const string LABEL_PARSING_ENABLED = "label-parsing-enabled"; public const string SHOW_SEEK_BY_10SECS = "show-seek-by-10secs"; public const string SHOW_BROWSE_ITEM_SEPARATORS = "show-browse-item-separators"; public class Settings { public const string LABEL_PARSING_ENABLED = "label-parsing-enabled"; public const string SHOW_SEEK_BY_10SECS = "show-seek-by-10secs"; public const string SHOW_BROWSE_ITEM_SEPARATORS = "show-browse-item-separators"; public GLib.Settings settings; public GLib.Settings settings; public Settings() { settings = new GLib.Settings("jp.pocka.plac.gtk-adwaita"); } public Settings() { settings = new GLib.Settings("jp.pocka.plac.gtk-adwaita"); } public string connected_server_id { owned get { return settings.get_string(CONNECTED_SERVER_ID); } set { settings.set_string(CONNECTED_SERVER_ID, value); } } public string connected_server_id { owned get { return settings.get_string(CONNECTED_SERVER_ID); } set { settings.set_string(CONNECTED_SERVER_ID, value); } } public string connected_server_token { owned get { return settings.get_string(CONNECTED_SERVER_TOKEN); } set { settings.set_string(CONNECTED_SERVER_TOKEN, value); } } public string connected_server_token { owned get { return settings.get_string(CONNECTED_SERVER_TOKEN); } set { settings.set_string(CONNECTED_SERVER_TOKEN, value); } } public string connected_server_addr { owned get { return settings.get_string(CONNECTED_SERVER_ADDR); } set { settings.set_string(CONNECTED_SERVER_ADDR, value); } } public string connected_server_addr { owned get { return settings.get_string(CONNECTED_SERVER_ADDR); } set { settings.set_string(CONNECTED_SERVER_ADDR, value); } } public uint16 connected_server_port { owned get { return (uint16) settings.get_uint(CONNECTED_SERVER_PORT); } set { settings.set_uint(CONNECTED_SERVER_PORT, value); } } public uint16 connected_server_port { owned get { return (uint16) settings.get_uint(CONNECTED_SERVER_PORT); } set { settings.set_uint(CONNECTED_SERVER_PORT, value); } } public bool label_parsing_enabled { owned get { return settings.get_boolean(LABEL_PARSING_ENABLED); } set { settings.set_boolean(LABEL_PARSING_ENABLED, value); } public bool label_parsing_enabled { owned get { return settings.get_boolean(LABEL_PARSING_ENABLED); } set { settings.set_boolean(LABEL_PARSING_ENABLED, value); } } } } }
-
-
-
@@ -15,145 +15,145 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [SingleInstance] private class ArtworkDownloader : Object { public Soup.Session session = new Soup.Session.with_options( "max_conns", 6, "max_conns_per_host", 6, "user_agent", "plac-for-gtk4/0.0 " ); [SingleInstance] private class ArtworkDownloader : Object { public Soup.Session session = new Soup.Session.with_options( "max_conns", 6, "max_conns_per_host", 6, "user_agent", "plac-for-gtk4/0.0 " ); // This class does not use Soup's cache feature: it's undocumented, // obscure usage (due both to API design and lack of documentation,) // and frequently crashes. As Roon Server seems to handle our current // request size/window/rate, this class avoids dealing with libsoup's // cache. public ArtworkDownloader() { Object(); // This class does not use Soup's cache feature: it's undocumented, // obscure usage (due both to API design and lack of documentation,) // and frequently crashes. As Roon Server seems to handle our current // request size/window/rate, this class avoids dealing with libsoup's // cache. public ArtworkDownloader() { Object(); } } } class Artwork : Gtk.Box { private ArtworkDownloader downloader = new ArtworkDownloader(); class Artwork : Gtk.Box { private ArtworkDownloader downloader = new ArtworkDownloader(); private uint16 _width = 100; public uint16 width { get { return _width; } construct set { _width = value; width_request = (int) value; private uint16 _width = 100; public uint16 width { get { return _width; } construct set { _width = value; width_request = (int) value; } } } private uint16 _height = 100; public uint16 height { get { return _height; } construct set { _height = value; height_request = (int) value; private uint16 _height = 100; public uint16 height { get { return _height; } construct set { _height = value; height_request = (int) value; } } } private Plac.AsyncConnection? _conn = null; public Plac.AsyncConnection? conn { get { return _conn; } set { if (_conn != value) { _conn = value; render(); private Plac.AsyncConnection? _conn = null; public Plac.AsyncConnection? conn { get { return _conn; } set { if (_conn != value) { _conn = value; render(); } } } } private string? _image_key = null; public string? image_key { get { return _image_key; } set { if (_image_key != value) { _image_key = value; render(); private string? _image_key = null; public string? image_key { get { return _image_key; } set { if (_image_key != value) { _image_key = value; render(); } } } } public Plac.Image.ScalingMethod scaling { get; set; default = FIT; } public Plac.Image.ScalingMethod scaling { get; set; default = FIT; } private Gtk.Picture picture = new Gtk.Picture(); private Adw.Spinner spinner = new Adw.Spinner(); private Gtk.Image error_icon = new Gtk.Image.from_icon_name("image-missing-symbolic"); private Gtk.Picture picture = new Gtk.Picture(); private Adw.Spinner spinner = new Adw.Spinner(); private Gtk.Image error_icon = new Gtk.Image.from_icon_name("image-missing-symbolic"); public Artwork() { Object(); } public Artwork.new_with_size(uint16 width, uint16 height) { Object(width: width, height: height); } public Artwork() { Object(); } construct { picture.set_parent(this); picture.accessible_role = PRESENTATION; picture.halign = CENTER; picture.valign = CENTER; picture.width_request = _width; picture.height_request = _height; picture.content_fit = SCALE_DOWN; picture.add_css_class("plac-playback-artwork"); picture.visible = false; public Artwork.new_with_size(uint16 width, uint16 height) { Object(width: width, height: height); } spinner.set_parent(this); spinner.halign = CENTER; spinner.valign = CENTER; spinner.width_request = _width; spinner.height_request = _height; spinner.visible = true; construct { picture.set_parent(this); picture.accessible_role = PRESENTATION; picture.halign = CENTER; picture.valign = CENTER; picture.width_request = _width; picture.height_request = _height; picture.content_fit = SCALE_DOWN; picture.add_css_class("plac-playback-artwork"); picture.visible = false; error_icon.set_parent(this); error_icon.halign = CENTER; error_icon.valign = CENTER; error_icon.width_request = _width; error_icon.height_request = _height; error_icon.pixel_size = _width / 2; error_icon.visible = false; spinner.set_parent(this); spinner.halign = CENTER; spinner.valign = CENTER; spinner.width_request = _width; spinner.height_request = _height; spinner.visible = true; render(); } error_icon.set_parent(this); error_icon.halign = CENTER; error_icon.valign = CENTER; error_icon.width_request = _width; error_icon.height_request = _height; error_icon.pixel_size = _width / 2; error_icon.visible = false; public void render() { picture.paintable = null; if (_conn == null || _image_key == null) { return; render(); } picture.visible = false; spinner.visible = true; error_icon.visible = false; public void render() { picture.paintable = null; if (_conn == null || _image_key == null) { return; } picture.visible = false; spinner.visible = true; error_icon.visible = false; var opts = new Plac.Image.GetOptions(); opts.set_size(scaling, _width, _height); opts.set_content_type(JPEG); var opts = new Plac.Image.GetOptions(); opts.set_size(scaling, _width, _height); opts.set_content_type(JPEG); GLib.Uri url; try { var raw = _conn.get_image_url(_image_key, opts); if (raw == null) { GLib.log("Plac", LEVEL_WARNING, "Failed to build image download URL for %s", _image_key); GLib.Uri url; try { var raw = _conn.get_image_url(_image_key, opts); if (raw == null) { GLib.log("Plac", LEVEL_WARNING, "Failed to build image download URL for %s", _image_key); error_icon.visible = true; spinner.visible = false; return; } url = GLib.Uri.parse(raw, NONE); } catch (GLib.UriError error) { GLib.log("Plac", LEVEL_WARNING, "Failed to build image download URL for %s: %s", _image_key, error.message); error_icon.visible = true; spinner.visible = false; return; } url = GLib.Uri.parse(raw, NONE); } catch (GLib.UriError error) { GLib.log("Plac", LEVEL_WARNING, "Failed to build image download URL for %s: %s", _image_key, error.message); error_icon.visible = true; spinner.visible = false; return; } var msg = new Soup.Message.from_uri("GET", url); var msg = new Soup.Message.from_uri("GET", url); downloader.session.send_and_read_async.begin(msg, 0, null, (obj, res) => { downloader.session.send_and_read_async.begin(msg, 0, null, (obj, res) => { try { var bytes = downloader.session.send_and_read_async.end(res);
-
@@ -182,6 +182,6 @@ spinner.visible = false;return; } }); } } } }
-
-
-
@@ -15,76 +15,76 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/browse.ui")] class Browse : Gtk.Box { public signal void loading_start(); public signal void loading_end(); [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/browse.ui")] class Browse : Gtk.Box { public signal void loading_start(); public signal void loading_end(); [GtkChild] private unowned Gtk.Label title; [GtkChild] private unowned Gtk.Label title; [GtkChild] private unowned Gtk.Label subtitle; [GtkChild] private unowned Gtk.Label subtitle; [GtkChild] private unowned Gtk.Box loading; [GtkChild] private unowned Gtk.Box loading; [GtkChild] private unowned Gtk.Button back; [GtkChild] private unowned Gtk.Button back; [GtkChild] private unowned Gtk.ListView items; [GtkChild] private unowned Gtk.ListView items; [GtkChild] private unowned Gtk.ScrolledWindow scroller; [GtkChild] private unowned Gtk.ScrolledWindow scroller; [GtkChild] private unowned Adw.ToastOverlay toasts; [GtkChild] private unowned Adw.ToastOverlay toasts; private GLib.ListStore items_store = new GLib.ListStore(typeof (BrowseItem.Item)); private GLib.ListStore items_store = new GLib.ListStore(typeof (BrowseItem.Item)); private Plac.AsyncConnection? conn = null; private Plac.AsyncConnection? conn = null; private Plac.Transport.Zone? _zone = null; public Plac.Transport.Zone? zone { get { return _zone; } set { _zone = value; load(); private Plac.Transport.Zone? _zone = null; public Plac.Transport.Zone? zone { get { return _zone; } set { _zone = value; load(); } } } private Plac.Browse.Hierarchy? _hierarchy = null; public Plac.Browse.Hierarchy hierarchy { get { return _hierarchy; } construct set { if (_hierarchy != value) { _hierarchy = value; item = null; load(); private Plac.Browse.Hierarchy? _hierarchy = null; public Plac.Browse.Hierarchy hierarchy { get { return _hierarchy; } construct set { if (_hierarchy != value) { _hierarchy = value; item = null; load(); } } } } private BrowseItem.Item? item = null; private BrowseItem.Item? item = null; private bool is_loading = false; private Settings settings = new Settings(); private bool is_loading = false; private Settings settings = new Settings(); public Browse() { Object(); } public Browse() { Object(); } construct { items.single_click_activate = true; items.model = new Gtk.NoSelection(items_store); construct { items.single_click_activate = true; items.model = new Gtk.NoSelection(items_store); var factory = new Gtk.SignalListItemFactory(); factory.setup.connect(BrowseItem.setup_browse_item); factory.bind.connect(BrowseItem.bind_browse_item); items.factory = factory; var factory = new Gtk.SignalListItemFactory(); factory.setup.connect(BrowseItem.setup_browse_item); factory.bind.connect(BrowseItem.bind_browse_item); items.factory = factory; items.activate.connect((position) => { items.activate.connect((position) => { var item = (BrowseItem.Item) items_store.get_item(position); if (item == null) { return;
-
@@ -94,45 +94,45 @@ this.item = item;load(false, false); }); back.clicked.connect(() => { back.clicked.connect(() => { pop(); }); settings.settings.bind(Settings.SHOW_BROWSE_ITEM_SEPARATORS, items, "show-separators", GET); } settings.settings.bind(Settings.SHOW_BROWSE_ITEM_SEPARATORS, items, "show-separators", GET); } public void start(Plac.AsyncConnection? conn) { this.conn = conn; this.load(); } public void start(Plac.AsyncConnection? conn) { this.conn = conn; this.load(); } private void start_loading() { is_loading = true; loading_start(); loading.visible = true; this.sensitive = false; } private void start_loading() { is_loading = true; loading_start(); loading.visible = true; this.sensitive = false; } private void end_loading() { is_loading = false; loading_end(); loading.visible = false; this.sensitive = true; } private void pop(bool scroll_to_top = true) { this.item = null; this.load(true, scroll_to_top); } private void end_loading() { is_loading = false; loading_end(); loading.visible = false; this.sensitive = true; } private void load(bool pop = false, bool scroll_to_top = true) { if (conn == null || _hierarchy == null || is_loading) { return; private void pop(bool scroll_to_top = true) { this.item = null; this.load(true, scroll_to_top); } var item = this.item == null ? null : this.item.item; start_loading(); conn.browse.begin(_hierarchy, _zone, item, pop, (obj, res) => { private void load(bool pop = false, bool scroll_to_top = true) { if (conn == null || _hierarchy == null || is_loading) { return; } var item = this.item == null ? null : this.item.item; start_loading(); conn.browse.begin(_hierarchy, _zone, item, pop, (obj, res) => { var result = conn.browse.end(res); if (result.code != OK) {
-
@@ -185,6 +185,6 @@back.visible = action.level > 0; end_loading(); }); } } } }
-
-
-
@@ -15,18 +15,18 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { class BrowseHierarchyRow : Gtk.ListBoxRow { public Plac.Browse.Hierarchy hierarchy { get; construct set; } public string label { get; construct set; } class BrowseHierarchyRow : Gtk.ListBoxRow { public Plac.Browse.Hierarchy hierarchy { get; construct set; } public string label { get; construct set; } public BrowseHierarchyRow(Plac.Browse.Hierarchy hierarchy, string label) { Object(hierarchy: hierarchy, label: label); } public BrowseHierarchyRow(Plac.Browse.Hierarchy hierarchy, string label) { Object(hierarchy: hierarchy, label: label); } construct { var text = new Gtk.Label(label); text.halign = START; this.child = text; construct { var text = new Gtk.Label(label); text.halign = START; this.child = text; } } } }
-
-
-
@@ -15,60 +15,60 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { namespace BrowseItem { class Item : Object { public Plac.Browse.Item item { get; construct; } public Plac.AsyncConnection? conn { get; construct; } namespace BrowseItem { class Item : Object { public Plac.Browse.Item item { get; construct; } public Plac.AsyncConnection? conn { get; construct; } public Item(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } } public Item(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } } // GListView requires a widget to calculate row sizes. private class RowContainer : Gtk.Box { private Gtk.Widget? child = null; private class RowContainer : Gtk.Box { private Gtk.Widget? child = null; private Item? _item = null; public Item? item { get { return _item; } private Item? _item = null; public Item? item { get { return _item; } construct set { _item = value; construct set { _item = value; if (child != null) { this.remove(child); child = null; } if (child != null) { this.remove(child); child = null; } if (value != null) { child = new Navigation(value.item, value.conn); this.append(child); if (value != null) { child = new Navigation(value.item, value.conn); this.append(child); } } } } } public RowContainer(Item? item = null) { Object(item: item); public RowContainer(Item? item = null) { Object(item: item); this.height_request = 48; } } this.height_request = 48; } } void setup_browse_item(Gtk.ListItemFactory factory, Object object) { var list_item = (Gtk.ListItem) object; list_item.child = new RowContainer(); list_item.selectable = false; } void setup_browse_item(Gtk.ListItemFactory factory, Object object) { var list_item = (Gtk.ListItem) object; list_item.child = new RowContainer(); list_item.selectable = false; } void bind_browse_item(Gtk.ListItemFactory factory, Object object) { var list_item = (Gtk.ListItem) object; var item = (Item) list_item.item; var container = (RowContainer) list_item.child; void bind_browse_item(Gtk.ListItemFactory factory, Object object) { var list_item = (Gtk.ListItem) object; var item = (Item) list_item.item; var container = (RowContainer) list_item.child; container.item = item; } } container.item = item; } } }
-
-
-
@@ -15,65 +15,65 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { namespace BrowseItem { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/browse-item-navigation.ui")] class Navigation : Gtk.Box { [GtkChild] private unowned Gtk.Label title; namespace BrowseItem { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/browse-item-navigation.ui")] class Navigation : Gtk.Box { [GtkChild] private unowned Gtk.Label title; [GtkChild] private unowned Gtk.Label subtitle; [GtkChild] private unowned Gtk.Label subtitle; [GtkChild] private unowned Artwork artwork; [GtkChild] private unowned Artwork artwork; [GtkChild] private unowned Gtk.Image icon; [GtkChild] private unowned Gtk.Image icon; public Plac.Browse.Item item { get; construct; } public Plac.Browse.Item item { get; construct; } private Settings settings = new Settings(); public Plac.AsyncConnection? conn { get { return artwork.conn; } set construct { artwork.conn = value; } } private Settings settings = new Settings(); public Navigation(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } public Plac.AsyncConnection? conn { get { return artwork.conn; } set construct { artwork.conn = value; } } construct { if (settings.label_parsing_enabled) { title.label = new Plac.Browse.Label(item.title).plain_text; } else { title.label = item.title; } if (item.subtitle != null) { if (settings.label_parsing_enabled) { subtitle.label = new Plac.Browse.Label(item.subtitle).plain_text; } else { subtitle.label = item.subtitle; public Navigation(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } subtitle.visible = true; } construct { if (settings.label_parsing_enabled) { title.label = new Plac.Browse.Label(item.title).plain_text; } else { title.label = item.title; } if (item.subtitle != null) { if (settings.label_parsing_enabled) { subtitle.label = new Plac.Browse.Label(item.subtitle).plain_text; } else { subtitle.label = item.subtitle; } subtitle.visible = true; } if (item.image_key != null) { artwork.conn = conn; artwork.image_key = item.image_key; artwork.visible = true; } if (item.image_key != null) { artwork.conn = conn; artwork.image_key = item.image_key; artwork.visible = true; } switch (item.hint) { case ACTION_LIST: case LIST: icon.icon_name = "go-next-symbolic"; break; default: icon.visible = false; break; switch (item.hint) { case ACTION_LIST: case LIST: icon.icon_name = "go-next-symbolic"; break; default: icon.visible = false; break; } } } } } } }
-
-
-
@@ -15,34 +15,34 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { namespace BrowseItem { class Row : Gtk.ListBoxRow { public Plac.Browse.Item item { get; construct; } namespace BrowseItem { class Row : Gtk.ListBoxRow { public Plac.Browse.Item item { get; construct; } public Plac.AsyncConnection? conn { get; construct; } public Plac.AsyncConnection? conn { get; construct; } public Row(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } public Row(Plac.Browse.Item item, Plac.AsyncConnection? conn = null) { Object(item: item, conn: conn); } construct { if (item.item_key != null) { switch (item.hint) { case LIST: case ACTION: case ACTION_LIST: this.activatable = true; break; default: this.activatable = false; break; construct { if (item.item_key != null) { switch (item.hint) { case LIST: case ACTION: case ACTION_LIST: this.activatable = true; break; default: this.activatable = false; break; } } this.child = new Navigation(item, conn); } } this.child = new Navigation(item, conn); } } } }
-
-
-
@@ -15,25 +15,25 @@ //// 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; [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; [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 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(); public GenericErrorDialog() { Object(); } } } }
-
-
-
@@ -15,197 +15,197 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/playback-toolbar.ui")] class PlaybackToolbar : Gtk.Box { public signal void zone_selected(Plac.Transport.Zone? zone); [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/playback-toolbar.ui")] class PlaybackToolbar : Gtk.Box { public signal void zone_selected(Plac.Transport.Zone? zone); [GtkChild] private unowned Gtk.Box controls; [GtkChild] private unowned Gtk.Box controls; [GtkChild] private unowned Gtk.Button play; [GtkChild] private unowned Gtk.Button play; [GtkChild] private unowned Gtk.Button pause; [GtkChild] private unowned Gtk.Button pause; [GtkChild] private unowned Gtk.Button prev; [GtkChild] private unowned Gtk.Button prev; [GtkChild] private unowned Gtk.Button next; [GtkChild] private unowned Gtk.Button next; [GtkChild] private unowned Gtk.Button seek_backwards_10; [GtkChild] private unowned Gtk.Button seek_backwards_10; [GtkChild] private unowned Gtk.Button seek_forward_10; [GtkChild] private unowned Gtk.Button seek_forward_10; [GtkChild] private unowned Gtk.Scale seek; [GtkChild] private unowned Gtk.Scale seek; [GtkChild] private unowned Gtk.Label playing_line1; [GtkChild] private unowned Gtk.Label playing_line1; [GtkChild] private unowned Gtk.Label playing_line2; [GtkChild] private unowned Gtk.Label playing_line2; [GtkChild] private unowned Gtk.Popover zone_list_popover; [GtkChild] private unowned Gtk.Popover zone_list_popover; [GtkChild] private unowned Gtk.DropDown zone_list; [GtkChild] private unowned Gtk.DropDown zone_list; [GtkChild] private unowned Gtk.ListBox zone_outputs; [GtkChild] private unowned Gtk.ListBox zone_outputs; [GtkChild] private unowned Artwork artwork; [GtkChild] private unowned Artwork artwork; private GLib.ListStore zones = new GLib.ListStore(typeof (ZoneWrapper)); private GLib.ListStore zones = new GLib.ListStore(typeof (ZoneWrapper)); private bool is_seeking = false; private int64? next_seek = null; private bool is_seeking = false; private int64? next_seek = null; private ZoneWrapper? _selected = null; private ZoneWrapper? selected { get { return _selected; } set { _selected = value; zone_selected(value.zone); next_seek = null; private ZoneWrapper? _selected = null; private ZoneWrapper? selected { get { return _selected; } set { _selected = value; zone_selected(value.zone); next_seek = null; } } } private Settings settings = new Settings(); private Plac.Transport.Zone? zone { get { if (selected != null) { return selected.zone; } private Settings settings = new Settings(); return null; } } private Plac.Transport.Zone? zone { get { if (selected != null) { return selected.zone; } private string? zone_id { get { if (zone != null) { return zone.id; return null; } } return null; } } private string? zone_id { get { if (zone != null) { return zone.id; } public Plac.AsyncConnection? conn { get { return artwork.conn; return null; } } set { if (artwork.conn != null) { artwork.conn.zones_changed.disconnect(on_zone_change); public Plac.AsyncConnection? conn { get { return artwork.conn; } artwork.conn = value; value.zones_changed.connect(on_zone_change); set { if (artwork.conn != null) { artwork.conn.zones_changed.disconnect(on_zone_change); } artwork.conn = value; value.zones_changed.connect(on_zone_change); } } } private void on_zone_change(Plac.Transport.ZoneListEvent event) { bool should_set_zone = this.selected == null; private void on_zone_change(Plac.Transport.ZoneListEvent event) { bool should_set_zone = this.selected == null; foreach (string id in event.removed) { for (int i = 0; i < zones.n_items; i++) { var item = (ZoneWrapper) zones.get_item(i); if (item != null && item.zone.id == id) { zones.remove(i); if (zone_id == id) { should_set_zone = true; foreach (string id in event.removed) { for (int i = 0; i < zones.n_items; i++) { var item = (ZoneWrapper) zones.get_item(i); if (item != null && item.zone.id == id) { zones.remove(i); if (zone_id == id) { should_set_zone = true; } break; } break; } GLib.log("Plac", LEVEL_DEBUG, "Zone id=%s removed", id); } GLib.log("Plac", LEVEL_DEBUG, "Zone id=%s removed", id); } foreach (Plac.Transport.Zone zone in event.added) { upsert_zone(zone); } foreach (Plac.Transport.Zone zone in event.added) { upsert_zone(zone); } foreach (Plac.Transport.Zone zone in event.changed) { upsert_zone(zone); } foreach (Plac.Transport.Zone zone in event.changed) { upsert_zone(zone); } foreach (Plac.Transport.SeekChange change in event.seek_changed) { for (int i = 0; i < zones.n_items; i++) { var item = (ZoneWrapper?) zones.get_item(i); if (item != null && item.zone.id == change.zone_id) { var zone = item.zone; if (zone.now_playing != null) { zone.now_playing.seek_position = change.seek_position; zone.now_playing.has_seek_position = true; foreach (Plac.Transport.SeekChange change in event.seek_changed) { for (int i = 0; i < zones.n_items; i++) { var item = (ZoneWrapper?) zones.get_item(i); if (item != null && item.zone.id == change.zone_id) { var zone = item.zone; if (zone.now_playing != null) { zone.now_playing.seek_position = change.seek_position; zone.now_playing.has_seek_position = true; } } } } } if (should_set_zone) { var first = zones.get_item(0); if (first != null) { this.selected = (ZoneWrapper) first; } else { this.selected = null; if (should_set_zone) { var first = zones.get_item(0); if (first != null) { this.selected = (ZoneWrapper) first; } else { this.selected = null; } } this.render(); } this.render(); } private void upsert_zone(Plac.Transport.Zone zone) { var payload = new ZoneWrapper(zone); private void upsert_zone(Plac.Transport.Zone zone) { var payload = new ZoneWrapper(zone); GLib.EqualFunc<ZoneWrapper>eq = ZoneWrapper.is_equal; uint found_position = 0; var found = zones.find_with_equal_func(payload, eq, out found_position); if (!found) { zones.append(payload); return; } GLib.EqualFunc<ZoneWrapper>eq = ZoneWrapper.is_equal; uint found_position = 0; var found = zones.find_with_equal_func(payload, eq, out found_position); if (!found) { zones.append(payload); return; var item = (ZoneWrapper) zones.get_item(found_position); item.zone = zone; } var item = (ZoneWrapper) zones.get_item(found_position); item.zone = zone; } public PlaybackToolbar() { Object(); } public PlaybackToolbar() { Object(); } construct { var zone_list_factory = new Gtk.SignalListItemFactory(); construct { var zone_list_factory = new Gtk.SignalListItemFactory(); zone_list_factory.setup.connect((item) => { zone_list_factory.setup.connect((item) => { var label = new Gtk.Label(""); ((Gtk.ListItem) item).child = label; }); zone_list_factory.bind.connect((item) => { zone_list_factory.bind.connect((item) => { var list_item = (Gtk.ListItem) item; var label = (Gtk.Label) list_item.child; var wrapper = (ZoneWrapper) list_item.item; label.label = wrapper.zone.name; }); zone_list.model = zones; zone_list.factory = zone_list_factory; zone_list.model = zones; zone_list.factory = zone_list_factory; zone_list.notify["selected"].connect(() => { zone_list.notify["selected"].connect(() => { this.selected = (ZoneWrapper) zones.get_item(zone_list.selected); this.render(); }); play.clicked.connect(() => { play.clicked.connect(() => { if (conn == null || zone == null) { return; }
-
@@ -217,7 +217,7 @@ this.enable_actions();}); }); pause.clicked.connect(() => { pause.clicked.connect(() => { if (conn == null || zone == null) { return; }
-
@@ -229,7 +229,7 @@ this.enable_actions();}); }); prev.clicked.connect(() => { prev.clicked.connect(() => { if (conn == null || zone == null) { return; }
-
@@ -241,7 +241,7 @@ this.enable_actions();}); }); next.clicked.connect(() => { next.clicked.connect(() => { if (conn == null || zone == null) { return; }
-
@@ -253,42 +253,42 @@ this.enable_actions();}); }); seek.change_value.connect((scroll, value) => { seek.change_value.connect((scroll, value) => { schedule_seek((int64) value); return false; }); seek_backwards_10.clicked.connect(() => { seek_backwards_10.clicked.connect(() => { var next_value = seek.get_value() - 10; seek.set_value(next_value); schedule_seek((int64) next_value); }); seek_forward_10.clicked.connect(() => { seek_forward_10.clicked.connect(() => { var next_value = seek.get_value() + 10; seek.set_value(next_value); schedule_seek((int64) next_value); }); seek.set_increments(1, 10); seek.set_increments(1, 10); settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, seek_backwards_10, "visible", GET); settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, seek_forward_10, "visible", GET); this.render(); } settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, seek_backwards_10, "visible", GET); settings.settings.bind(Settings.SHOW_SEEK_BY_10SECS, seek_forward_10, "visible", GET); private void dequeue_seek() { if (conn == null || next_seek == null || zone == null) { is_seeking = false; return; this.render(); } var position = next_seek; next_seek = null; private void dequeue_seek() { if (conn == null || next_seek == null || zone == null) { is_seeking = false; return; } conn.seek.begin(zone, position, (obj, res) => { var position = next_seek; next_seek = null; conn.seek.begin(zone, position, (obj, res) => { var result = conn.seek.end(res); if (result != OK) {
-
@@ -297,172 +297,172 @@ }dequeue_seek(); }); } } private void schedule_seek(int64 position) { next_seek = position; private void schedule_seek(int64 position) { next_seek = position; if (!is_seeking) { is_seeking = true; dequeue_seek(); if (!is_seeking) { is_seeking = true; dequeue_seek(); } } } private string format_seconds(uint64 secs) { var s = secs % 60; var m = (secs / 60) % 60; var h = secs / 360; private string format_seconds(uint64 secs) { var s = secs % 60; var m = (secs / 60) % 60; var h = secs / 360; if (h == 0) { return "%02u:%02u".printf((uint) m, (uint) s); } if (h == 0) { return "%02u:%02u".printf((uint) m, (uint) s); // I don't care 32 bit platforms. Also I doubt Roon can handle a song // of a duration longer than max(uint32) return "%02u:%02u:%02u".printf((uint) h, (uint) m, (uint) s); } // I don't care 32 bit platforms. Also I doubt Roon can handle a song // of a duration longer than max(uint32) return "%02u:%02u:%02u".printf((uint) h, (uint) m, (uint) s); } private void render() { var zone = this.zone; if (zone == null) { play.visible = true; pause.visible = false; return; } private void render() { var zone = this.zone; if (zone == null) { play.visible = true; pause.visible = false; return; } play.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PLAY) != 0; pause.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PAUSE) != 0; prev.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PREV) != 0; next.sensitive = (zone.allowed_action & Plac.Transport.ACTION_NEXT) != 0; play.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PLAY) != 0; pause.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PAUSE) != 0; prev.sensitive = (zone.allowed_action & Plac.Transport.ACTION_PREV) != 0; next.sensitive = (zone.allowed_action & Plac.Transport.ACTION_NEXT) != 0; if (zone.now_playing != null) { playing_line1.label = zone.now_playing.two_line_line1; playing_line2.label = zone.now_playing.two_line_line2 != null ? zone.now_playing.two_line_line2 : ""; artwork.image_key = zone.now_playing.image_key; } else { playing_line1.label = ""; playing_line2.label = ""; artwork.image_key = null; } if (zone.now_playing != null) { playing_line1.label = zone.now_playing.two_line_line1; playing_line2.label = zone.now_playing.two_line_line2 != null ? zone.now_playing.two_line_line2 : ""; artwork.image_key = zone.now_playing.image_key; } else { playing_line1.label = ""; playing_line2.label = ""; artwork.image_key = null; } if ( zone.now_playing != null && zone.now_playing.has_length && zone.now_playing.has_seek_position ) { seek.sensitive = true; seek.set_range(0, zone.now_playing.length); seek.set_value(zone.now_playing.seek_position); seek.tooltip_text = format_seconds(zone.now_playing.seek_position); } else { seek.sensitive = false; seek.set_range(0, 1); seek.set_value(0); seek.tooltip_text = "Not playing"; } if ( zone.now_playing != null && zone.now_playing.has_length && zone.now_playing.has_seek_position ) { seek.sensitive = true; seek.set_range(0, zone.now_playing.length); seek.set_value(zone.now_playing.seek_position); seek.tooltip_text = format_seconds(zone.now_playing.seek_position); } else { seek.sensitive = false; seek.set_range(0, 1); seek.set_value(0); seek.tooltip_text = "Not playing"; } switch (zone.playback) { case LOADING: case STOPPED: case PAUSED: play.visible = true; pause.visible = false; break; case PLAYING: play.visible = false; pause.visible = true; break; } switch (zone.playback) { case LOADING: case STOPPED: case PAUSED: play.visible = true; pause.visible = false; break; case PLAYING: play.visible = false; pause.visible = true; break; } stale_outputs(); stale_outputs(); foreach (var output in zone.outputs) { update_output(output); } foreach (var output in zone.outputs) { update_output(output); remove_stale_outputs(); } remove_stale_outputs(); } private void stale_outputs() { int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { return; } private void stale_outputs() { int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { return; row.stale = true; } row.stale = true; } } private void remove_stale_outputs() { var rows_to_remove = new Gee.ArrayList<ZoneOutputRow>(); private void remove_stale_outputs() { var rows_to_remove = new Gee.ArrayList<ZoneOutputRow>(); int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { break; int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { break; } if (row.stale) { rows_to_remove.add(row); } } if (row.stale) { rows_to_remove.add(row); foreach (var row in rows_to_remove) { zone_outputs.remove(row); } } foreach (var row in rows_to_remove) { zone_outputs.remove(row); } } private void update_output(Plac.Transport.Output output) { int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { var new_row = new ZoneOutputRow(output); new_row.conn = conn; this.bind_property("conn", new_row, "conn", DEFAULT); zone_outputs.append(new_row); return; } private void update_output(Plac.Transport.Output output) { int i = 0; while (true) { var row = (ZoneOutputRow?) zone_outputs.get_row_at_index(i++); if (row == null) { var new_row = new ZoneOutputRow(output); new_row.conn = conn; this.bind_property("conn", new_row, "conn", DEFAULT); zone_outputs.append(new_row); if (row.output_id != output.id) { continue; } row.output = output; row.stale = false; return; } if (row.output_id != output.id) { continue; } row.output = output; row.stale = false; return; } } private void disable_actions() { zone_list_popover.sensitive = false; controls.sensitive = false; seek.sensitive = false; } private void disable_actions() { zone_list_popover.sensitive = false; controls.sensitive = false; seek.sensitive = false; } private void enable_actions() { zone_list_popover.sensitive = true; controls.sensitive = true; seek.sensitive = true; private void enable_actions() { zone_list_popover.sensitive = true; controls.sensitive = true; seek.sensitive = true; } } } // GLib and its ecosystem heavily relies on GObject. // Wrapping a class to avoid GLib from infecting to other parts. class ZoneWrapper : Object { public Plac.Transport.Zone zone { get; set construct; } class ZoneWrapper : Object { public Plac.Transport.Zone zone { get; set construct; } public ZoneWrapper(Plac.Transport.Zone zone) { Object(zone: zone); } public ZoneWrapper(Plac.Transport.Zone zone) { Object(zone: zone); } public static bool is_equal(ZoneWrapper a, ZoneWrapper b) { return a.zone.id == b.zone.id; public static bool is_equal(ZoneWrapper a, ZoneWrapper b) { return a.zone.id == b.zone.id; } } } }
-
-
-
@@ -15,124 +15,124 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/zone-output-row.ui")] class ZoneOutputRow : Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Label display_name; [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/zone-output-row.ui")] class ZoneOutputRow : Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Label display_name; [GtkChild] private unowned Gtk.Box incremental_control; [GtkChild] private unowned Gtk.Box incremental_control; [GtkChild] private unowned Gtk.Box value_control; [GtkChild] private unowned Gtk.Box value_control; [GtkChild] private unowned Gtk.Scale volume_slider; [GtkChild] private unowned Gtk.Scale volume_slider; [GtkChild] private unowned Gtk.Button volume_up_incr; [GtkChild] private unowned Gtk.Button volume_up_incr; [GtkChild] private unowned Gtk.Button volume_up; [GtkChild] private unowned Gtk.Button volume_up; [GtkChild] private unowned Gtk.Button volume_down_incr; [GtkChild] private unowned Gtk.Button volume_down_incr; [GtkChild] private unowned Gtk.Button volume_down; [GtkChild] private unowned Gtk.Button volume_down; public bool stale = false; public bool stale = false; public string output_id { get { return _output.id; } } public string output_id { get { return _output.id; } } public Plac.AsyncConnection? conn { get; set construct; } public Plac.AsyncConnection? conn { get; set construct; } private Plac.Transport.Output _output; public Plac.Transport.Output output { get { return _output; } set construct { _output = value; display_name.label = value.display_name; if (value.is_incremental_volume) { incremental_control.visible = true; value_control.visible = false; } else { incremental_control.visible = false; value_control.visible = true; if (volume_slider.adjustment.lower != output.volume.min) { volume_slider.adjustment.lower = output.volume.min; } if (volume_slider.adjustment.upper != output.volume.max) { volume_slider.adjustment.upper = output.volume.max; } if (!is_moving_volume && volume_slider.adjustment.value != output.volume.value) { volume_slider.adjustment.value = output.volume.value; } if (volume_slider.adjustment.step_increment != output.volume.step) { volume_slider.adjustment.step_increment = output.volume.step; private Plac.Transport.Output _output; public Plac.Transport.Output output { get { return _output; } set construct { _output = value; display_name.label = value.display_name; if (value.is_incremental_volume) { incremental_control.visible = true; value_control.visible = false; } else { incremental_control.visible = false; value_control.visible = true; if (volume_slider.adjustment.lower != output.volume.min) { volume_slider.adjustment.lower = output.volume.min; } if (volume_slider.adjustment.upper != output.volume.max) { volume_slider.adjustment.upper = output.volume.max; } if (!is_moving_volume && volume_slider.adjustment.value != output.volume.value) { volume_slider.adjustment.value = output.volume.value; } if (volume_slider.adjustment.step_increment != output.volume.step) { volume_slider.adjustment.step_increment = output.volume.step; } } } } } private bool is_incrementing_volume = false; private bool is_incrementing_volume = false; // true = increase, false = decrease private Gee.ArrayQueue<bool>volume_increment_queue = new Gee.ArrayQueue<bool>(); // true = increase, false = decrease private Gee.ArrayQueue<bool>volume_increment_queue = new Gee.ArrayQueue<bool>(); private bool is_moving_volume = false; private double? next_volume_move_to = null; private bool is_moving_volume = false; private double? next_volume_move_to = null; public ZoneOutputRow(Plac.Transport.Output output) { Object(output: output); } public ZoneOutputRow(Plac.Transport.Output output) { Object(output: output); } construct { volume_up_incr.clicked.connect(() => { construct { volume_up_incr.clicked.connect(() => { queue_incremental_volume_change(true); }); volume_up.clicked.connect(() => { volume_up.clicked.connect(() => { var current = next_volume_move_to != null ? next_volume_move_to : output.volume.value; schedule_volume_change(current + output.volume.step); }); volume_down_incr.clicked.connect(() => { volume_down_incr.clicked.connect(() => { queue_incremental_volume_change(false); }); volume_down.clicked.connect(() => { volume_down.clicked.connect(() => { var current = next_volume_move_to != null ? next_volume_move_to : output.volume.value; schedule_volume_change(current - output.volume.step); }); volume_slider.change_value.connect((scroll, value) => { volume_slider.change_value.connect((scroll, value) => { schedule_volume_change(value); return false; }); } } private void queue_incremental_volume_change(bool is_increase) { volume_increment_queue.offer(is_increase); private void queue_incremental_volume_change(bool is_increase) { volume_increment_queue.offer(is_increase); if (!is_incrementing_volume) { is_incrementing_volume = true; dequeue_incremental_volume_change(); if (!is_incrementing_volume) { is_incrementing_volume = true; dequeue_incremental_volume_change(); } } } private void dequeue_incremental_volume_change() { if (conn == null || volume_increment_queue.is_empty) { is_incrementing_volume = false; return; } private void dequeue_incremental_volume_change() { if (conn == null || volume_increment_queue.is_empty) { is_incrementing_volume = false; return; } var next = volume_increment_queue.poll(); var next = volume_increment_queue.poll(); if (next) { conn.increase_volume.begin(output, (obj, res) => { if (next) { conn.increase_volume.begin(output, (obj, res) => { var code = conn.increase_volume.end(res); if (code != OK) { GLib.log("Plac", LEVEL_WARNING, "Increase volume failed: %s", code.to_string());
-
@@ -140,8 +140,8 @@ }dequeue_incremental_volume_change(); }); } else { conn.decrease_volume.begin(output, (obj, res) => { } else { conn.decrease_volume.begin(output, (obj, res) => { var code = conn.decrease_volume.end(res); if (code != OK) { GLib.log("Plac", LEVEL_WARNING, "Decrease volume failed: %s", code.to_string());
-
@@ -149,28 +149,28 @@ }dequeue_incremental_volume_change(); }); } } } private void schedule_volume_change(double value) { next_volume_move_to = value; private void schedule_volume_change(double value) { next_volume_move_to = value; if (!is_moving_volume) { is_moving_volume = true; take_volume_change(); if (!is_moving_volume) { is_moving_volume = true; take_volume_change(); } } } private void take_volume_change() { if (conn == null || next_volume_move_to == null) { is_moving_volume = false; return; } private void take_volume_change() { if (conn == null || next_volume_move_to == null) { is_moving_volume = false; return; } var value = next_volume_move_to; next_volume_move_to = null; var value = next_volume_move_to; next_volume_move_to = null; conn.change_volume.begin(output, value, (obj, res) => { conn.change_volume.begin(output, value, (obj, res) => { var code = conn.change_volume.end(res); if (code != OK) { GLib.log("Plac", LEVEL_WARNING, "Change volume failed: %s", code.to_string());
-
@@ -178,6 +178,6 @@ }take_volume_change(); }); } } } }
-
-
-
@@ -15,83 +15,83 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { private errordomain ResolveError { CONNECTION_ERROR, SERVER_NOT_FOUND } private errordomain ResolveError { CONNECTION_ERROR, SERVER_NOT_FOUND } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/main-window.ui")] class MainWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.Stack root_stack; [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/main-window.ui")] class MainWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.Stack root_stack; [GtkChild] private unowned PlaybackToolbar playback_toolbar; [GtkChild] private unowned PlaybackToolbar playback_toolbar; [GtkChild] private unowned Adw.Banner error_banner; [GtkChild] private unowned Adw.Banner error_banner; [GtkChild] private unowned Browse browse; [GtkChild] private unowned Browse browse; [GtkChild] private unowned Gtk.ListBox browse_hierarchy; [GtkChild] private unowned Gtk.ListBox browse_hierarchy; private Settings settings = new Settings(); private Settings settings = new Settings(); private Plac.Discovery.Server? server = null; private Plac.AsyncConnection? conn = null; private Plac.Discovery.Server? server = null; private Plac.AsyncConnection? conn = null; private string server_id; private string server_id; public MainWindow(Gtk.Application app, Plac.Discovery.Server server) { (typeof (Artwork)).ensure(); (typeof (ServerConnecting)).ensure(); (typeof (PlaybackToolbar)).ensure(); public MainWindow(Gtk.Application app, Plac.Discovery.Server server) { (typeof (Artwork)).ensure(); (typeof (ServerConnecting)).ensure(); (typeof (PlaybackToolbar)).ensure(); Object(application: app); this.server = server; this.conn = new Plac.AsyncConnection(server); this.server_id = server.id; } Object(application: app); this.server = server; this.conn = new Plac.AsyncConnection(server); this.server_id = server.id; } public MainWindow.from_server_id(Gtk.Application app, string server_id) { (typeof (Artwork)).ensure(); (typeof (ServerConnecting)).ensure(); (typeof (PlaybackToolbar)).ensure(); public MainWindow.from_server_id(Gtk.Application app, string server_id) { (typeof (Artwork)).ensure(); (typeof (ServerConnecting)).ensure(); (typeof (PlaybackToolbar)).ensure(); Object(application: app); this.server_id = server_id; } Object(application: app); this.server_id = server_id; } construct { var provider = new Gtk.CssProvider(); provider.load_from_resource("/jp/pocka/plac/gtk-adwaita/css/main-window.css"); Gtk.StyleContext.add_provider_for_display( this.display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); construct { var provider = new Gtk.CssProvider(); provider.load_from_resource("/jp/pocka/plac/gtk-adwaita/css/main-window.css"); Gtk.StyleContext.add_provider_for_display( this.display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); error_banner.button_clicked.connect(() => { error_banner.button_clicked.connect(() => { try_listen(); }); playback_toolbar.zone_selected.connect((zone) => { playback_toolbar.zone_selected.connect((zone) => { browse.zone = zone; }); var explore_row = new BrowseHierarchyRow(BROWSE, "Explore"); browse_hierarchy.append(explore_row); browse_hierarchy.select_row(explore_row); var explore_row = new BrowseHierarchyRow(BROWSE, "Explore"); browse_hierarchy.append(explore_row); browse_hierarchy.select_row(explore_row); browse_hierarchy.append(new BrowseHierarchyRow(ALBUMS, "Albums")); browse_hierarchy.append(new BrowseHierarchyRow(PLAYLISTS, "Playlists")); browse_hierarchy.append(new BrowseHierarchyRow(ARTISTS, "Artists")); browse_hierarchy.append(new BrowseHierarchyRow(GENRES, "Genres")); browse_hierarchy.append(new BrowseHierarchyRow(COMPOSERS, "Composers")); browse_hierarchy.append(new BrowseHierarchyRow(INTERNET_RADIO, "Internet Radio")); browse_hierarchy.append(new BrowseHierarchyRow(SETTINGS, "Settings")); browse_hierarchy.append(new BrowseHierarchyRow(ALBUMS, "Albums")); browse_hierarchy.append(new BrowseHierarchyRow(PLAYLISTS, "Playlists")); browse_hierarchy.append(new BrowseHierarchyRow(ARTISTS, "Artists")); browse_hierarchy.append(new BrowseHierarchyRow(GENRES, "Genres")); browse_hierarchy.append(new BrowseHierarchyRow(COMPOSERS, "Composers")); browse_hierarchy.append(new BrowseHierarchyRow(INTERNET_RADIO, "Internet Radio")); browse_hierarchy.append(new BrowseHierarchyRow(SETTINGS, "Settings")); browse_hierarchy.row_selected.connect((row) => { browse_hierarchy.row_selected.connect((row) => { if (row == null) { return; }
-
@@ -99,39 +99,38 @@browse.hierarchy = ((BrowseHierarchyRow) row).hierarchy; }); browse.loading_start.connect(() => { browse.loading_start.connect(() => { browse_hierarchy.sensitive = false; }); browse.loading_end.connect(() => { browse.loading_end.connect(() => { browse_hierarchy.sensitive = true; }); } } public void start() { root_stack.visible_child_name = "loading"; public void start() { root_stack.visible_child_name = "loading"; try_listen(); try_listen(); this.present(); } this.present(); } public void start_with_addr(string ip_addr, uint16 port) { root_stack.visible_child_name = "loading"; public void start_with_addr(string ip_addr, uint16 port) { root_stack.visible_child_name = "loading"; try_listen_with_addr(ip_addr, port); try_listen_with_addr(ip_addr, port); this.present(); } this.present(); } private void try_listen() { error_banner.revealed = false; private void try_listen() { error_banner.revealed = false; if (conn != null) { listen_events(); } else { resolve_server.begin((obj, res) => { if (conn != null) { listen_events(); } else { resolve_server.begin((obj, res) => { try { resolve_server.end(res); } catch (ResolveError e) {
-
@@ -142,16 +141,16 @@ }listen_events(); }); } } } private void try_listen_with_addr(string ip_addr, uint16 port) { if (conn != null) { listen_events(); return; } private void try_listen_with_addr(string ip_addr, uint16 port) { if (conn != null) { listen_events(); return; } resolve_server_with_addr.begin(ip_addr, port, (obj, res) => { resolve_server_with_addr.begin(ip_addr, port, (obj, res) => { try { resolve_server_with_addr.end(res); } catch (ResolveError e) {
-
@@ -162,17 +161,17 @@ }listen_events(); }); } } private void listen_events() { this.title = "Plac - %s".printf(server.name); private void listen_events() { this.title = "Plac - %s".printf(server.name); conn.connection_started.connect(() => { conn.connection_started.connect(() => { root_stack.visible_child_name = "loading"; playback_toolbar.visible = false; }); conn.connected.connect((event) => { conn.connected.connect((event) => { root_stack.visible_child_name = "main"; playback_toolbar.visible = true;
-
@@ -184,28 +183,28 @@browse.start(conn); }); conn.connection_error.connect((event) => { conn.connection_error.connect((event) => { GLib.log("Plac", LEVEL_CRITICAL, "Failed to connect: %s", event.code.to_string()); error_banner.title = "Connection error: %s".printf(event.code.to_string()); error_banner.revealed = true; }); conn.out_of_memory_error.connect(() => { conn.out_of_memory_error.connect(() => { GLib.log("Plac", LEVEL_CRITICAL, "Failed to connect: out of memory"); error_banner.title = "Connection error (out of memory)"; error_banner.revealed = true; }); playback_toolbar.conn = conn; playback_toolbar.conn = conn; conn.activate(); } conn.activate(); } private async void resolve_server() throws ResolveError { GLib.SourceFunc callback = resolve_server.callback; ResolveError? error = null; private async void resolve_server() throws ResolveError { GLib.SourceFunc callback = resolve_server.callback; ResolveError? error = null; Plac.Discovery.find_async.begin(server_id, (obj, res) => { Plac.Discovery.find_async.begin(server_id, (obj, res) => { var result = Plac.Discovery.find_async.end(res); if (result.code != OK) {
-
@@ -227,18 +226,18 @@ conn = new Plac.AsyncConnection.with_token(server, settings.connected_server_token);Idle.add((owned) callback); }); yield; yield; if (error != null) { throw error; if (error != null) { throw error; } } } private async void resolve_server_with_addr(string ip_addr, uint16 http_port) throws ResolveError { GLib.SourceFunc callback = resolve_server_with_addr.callback; ResolveError? error = null; private async void resolve_server_with_addr(string ip_addr, uint16 http_port) throws ResolveError { GLib.SourceFunc callback = resolve_server_with_addr.callback; ResolveError? error = null; Plac.Discovery.resolve_async.begin(server_id, ip_addr, http_port, (obj, res) => { Plac.Discovery.resolve_async.begin(server_id, ip_addr, http_port, (obj, res) => { var result = Plac.Discovery.resolve_async.end(res); if (result.code != OK) {
-
@@ -262,11 +261,11 @@ conn = new Plac.AsyncConnection.with_token(server, settings.connected_server_token);Idle.add((owned) callback); }); yield; yield; if (error != null) { throw error; if (error != null) { throw error; } } } } }
-
-
-
@@ -15,55 +15,55 @@ //// SPDX-License-Identifier: Apache-2.0 namespace PlacGtkAdwaita { namespace ServerSelector { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list.ui")] class Window : Adw.ApplicationWindow { private enum ScanErrorKind { UNEXPECTED_ERROR, NETWORK_ERROR, } namespace ServerSelector { [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list.ui")] class Window : Adw.ApplicationWindow { private enum ScanErrorKind { UNEXPECTED_ERROR, NETWORK_ERROR, } [GtkChild] private unowned Gtk.ListBox servers_list; [GtkChild] private unowned Gtk.ListBox servers_list; [GtkChild] private unowned Adw.Banner failure_banner; [GtkChild] private unowned Adw.Banner failure_banner; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Adw.StatusPage empty; [GtkChild] private unowned Adw.StatusPage empty; private ulong error_detail_hid; private ulong error_detail_hid; public Window(Gtk.Application app) { Object(application: app); } public Window(Gtk.Application app) { Object(application: app); } public void start() { var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(this.scan); this.add_action(scan_action); public void start() { var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(this.scan); this.add_action(scan_action); this.present(); this.present(); this.scan(); } this.scan(); } private void scan() { 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.sensitive = false; private void scan() { 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.sensitive = false; Plac.Discovery.scan_async.begin((obj, res) => { Plac.Discovery.scan_async.begin((obj, res) => { var result = Plac.Discovery.scan_async.end(res); servers_list.remove_all();
-
@@ -123,12 +123,12 @@stack.visible_child_name = "idle"; scan_button.sensitive = true; }); } } private void show_error(ScanErrorKind kind, string message) { scan_button.add_css_class("suggested-action"); private void show_error(ScanErrorKind kind, string message) { scan_button.add_css_class("suggested-action"); error_detail_hid = failure_banner.button_clicked.connect(() => { error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message);
-
@@ -141,10 +141,10 @@ break;} }); failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.sensitive = true; failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.sensitive = true; } } } } } }
-