Changes
7 changed files (+351/-90)
-
-
@@ -28,6 +28,9 @@ <file preprocess="xml-stripblanks">ui/generic-error-dialog.ui</file><file preprocess="xml-stripblanks">ui/server-connecting.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> <file preprocess="xml-stripblanks">ui/zone-output-row.ui</file> <file preprocess="xml-stripblanks">icons/scalable/actions/audio-volume-high-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/audio-volume-low-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/go-next-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/go-previous-symbolic.svg</file> <file preprocess="xml-stripblanks">icons/scalable/actions/image-missing-symbolic.svg</file>
-
-
-
@@ -0,0 +1,8 @@<?xml version="1.0" encoding="UTF-8"?> <!-- SPDX-FileCopyrightText: Gnome Developers SPDX-License-Identifier: CC0-1.0 --> <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"> <path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 6.460938 0.960938 c -0.191407 -0.003906 -0.386719 0.054688 -0.558594 0.167969 c -0.457032 0.3125 -0.578125 0.933593 -0.269532 1.390625 c 1.824219 2.707031 1.824219 6.238281 0 8.945312 c -0.308593 0.457032 -0.1875 1.078125 0.269532 1.390625 c 0.457031 0.308594 1.078125 0.1875 1.390625 -0.269531 c 1.136719 -1.691406 1.707031 -3.640625 1.707031 -5.59375 s -0.570312 -3.902344 -1.707031 -5.59375 c -0.195313 -0.285156 -0.511719 -0.4375 -0.832031 -0.4375 z m -3.421876 2.019531 c -0.222656 -0.007812 -0.453124 0.058594 -0.644531 0.203125 c -0.261719 0.199219 -0.394531 0.5 -0.394531 0.804688 v 0.058594 c 0.011719 0.191406 0.074219 0.375 0.199219 0.535156 c 1.074219 1.429687 1.074219 3.390625 0 4.816406 c -0.125 0.164062 -0.1875 0.347656 -0.199219 0.535156 v 0.0625 c 0 0.304688 0.132812 0.605469 0.394531 0.804688 c 0.441407 0.332031 1.066407 0.242187 1.398438 -0.199219 c 0.804687 -1.066406 1.207031 -2.335937 1.207031 -3.609375 s -0.402344 -2.542969 -1.207031 -3.613281 c -0.183594 -0.246094 -0.464844 -0.382813 -0.753907 -0.398438 z m 0 0" fill="#2e3436"/> </svg>
-
-
-
@@ -0,0 +1,8 @@<?xml version="1.0" encoding="UTF-8"?> <!-- SPDX-FileCopyrightText: Gnome Developers SPDX-License-Identifier: CC0-1.0 --> <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"> <path d="m 7 1.007812 c -0.296875 -0.003906 -0.578125 0.125 -0.769531 0.351563 l -3.230469 3.640625 h -1 c -1.09375 0 -2 0.84375 -2 2 v 2 c 0 1.089844 0.910156 2 2 2 h 1 l 3.230469 3.640625 c 0.210937 0.253906 0.492187 0.363281 0.769531 0.359375 z m 2.957031 2.980469 c -0.199219 0.011719 -0.394531 0.074219 -0.5625 0.203125 c -0.441406 0.332032 -0.53125 0.960938 -0.195312 1.402344 c 1.074219 1.425781 1.074219 3.386719 0 4.8125 c -0.335938 0.441406 -0.246094 1.070312 0.195312 1.402344 c 0.441407 0.332031 1.066407 0.242187 1.398438 -0.195313 c 0.804687 -1.070312 1.207031 -2.339843 1.207031 -3.613281 s -0.402344 -2.542969 -1.207031 -3.613281 c -0.183594 -0.246094 -0.464844 -0.382813 -0.753907 -0.398438 c -0.027343 0 -0.054687 0 -0.085937 0 z m 0 0" fill="#2e3436"/> </svg>
-
-
-
@@ -76,15 +76,26 @@ <object class="GtkMenuButton"><style> <class name="flat" /> </style> <property name="direction">up</property> <property name="label">Zone</property> <property name="valign">start</property> <property name="popover"> <object class="GtkPopover" id="zone_list_popover"> <child> <object class="GtkListBox" id="zone_list"> <style> <class name="navigation-sidebar" /> </style> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">8</property> <child> <object class="GtkDropDown" id="zone_list" /> </child> <child> <object class="GtkListBox" id="zone_outputs"> <property name="selection-mode">none</property> <style> <class name="boxed-list-separate" /> </style> </object> </child> </object> </child> </object>
-
-
-
@@ -0,0 +1,95 @@<?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="PlacGtkAdwaitaZoneOutputRow" parent="GtkListBoxRow"> <property name="activatable">false</property> <property name="width-request">220</property> <child> <object class="GtkBox"> <property name="orientation">vertical</property> <property name="spacing">6</property> <property name="margin-top">4</property> <binding name="margin-bottom"> <lookup name="margin-top" type="GtkBox" /> </binding> <property name="margin-start">6</property> <binding name="margin-end"> <lookup name="margin-start" type="GtkBox" /> </binding> <child> <object class="GtkLabel" id="display_name"> <property name="halign">start</property> </object> </child> <child> <object class="GtkBox" id="incremental_control"> <property name="spacing">6</property> <child> <object class="GtkButton"> <property name="label">Volume down</property> <property name="icon-name">audio-volume-low-symbolic</property> <property name="hexpand">true</property> </object> </child> <child> <object class="GtkButton"> <property name="label">Volume up</property> <property name="icon-name">audio-volume-high-symbolic</property> <property name="hexpand">true</property> </object> </child> </object> </child> <child> <object class="GtkBox" id="value_control"> <child> <object class="GtkButton"> <property name="label">Volume down</property> <property name="icon-name">audio-volume-low-symbolic</property> <style> <class name="flat" /> <class name="circular" /> </style> </object> </child> <child> <object class="GtkScale" id="volume_slider"> <property name="hexpand">true</property> <property name="orientation">horizontal</property> <accessibility> <property name="label">Seekbar</property> </accessibility> </object> </child> <child> <object class="GtkButton"> <property name="label">Volume up</property> <property name="icon-name">audio-volume-high-symbolic</property> <style> <class name="flat" /> <class name="circular" /> </style> </object> </child> </object> </child> </object> </child> </template> </interface>
-
-
-
@@ -53,38 +53,48 @@ [GtkChild]private unowned Gtk.Popover zone_list_popover; [GtkChild] private unowned Gtk.ListBox zone_list; private unowned Gtk.DropDown zone_list; [GtkChild] private unowned Gtk.ListBox zone_outputs; [GtkChild] private unowned Artwork artwork; private Gee.HashMap<string, ZoneSelectorRow>zone_rows = new Gee.HashMap<string, ZoneSelectorRow>(); private GLib.ListStore zones = new GLib.ListStore(typeof (ZoneWrapper)); private bool is_seeking = false; private int64? next_seek = null; private string? zone_id = 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 { var row = zone_rows[zone_id]; if (row == null) { return null; if (selected != null) { return selected.zone; } return row.zone; return null; } } set { if (value == null) { zone_id = null; } else { zone_id = value.id; private string? zone_id { get { if (zone != null) { return zone.id; } zone_selected(value); next_seek = null; return null; } }
-
@@ -104,129 +114,140 @@ }} 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; } break; } } GLib.log("Plac", LEVEL_DEBUG, "Zone id=%s removed", id); if (zone_rows.has_key(id)) { var existing = zone_rows[id]; zone_rows.unset(id); zone_list.remove(existing); } } foreach (Plac.Transport.Zone zone in event.added) { if (zone_rows.has_key(zone.id)) { var existing = zone_rows[zone.id]; existing.zone = zone; existing.render(); } else { var row = new ZoneSelectorRow(zone); zone_rows[zone.id] = row; zone_list.append(row); if (zone_id == null) { this.zone = zone; } } upsert_zone(zone); } foreach (Plac.Transport.Zone zone in event.changed) { if (zone_rows.has_key(zone.id)) { var existing = zone_rows[zone.id]; existing.zone = zone; existing.render(); } else { var row = new ZoneSelectorRow(zone); zone_rows[zone.id] = row; zone_list.append(row); if (zone_id == null) { this.zone = zone; } } upsert_zone(zone); } foreach (Plac.Transport.SeekChange change in event.seek_changed) { if (zone_rows.has_key(change.zone_id)) { var zone = zone_rows[change.zone_id].zone; if (zone.now_playing != null) { zone.now_playing.seek_position = change.seek_position; zone.now_playing.has_seek_position = true; 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; } } } } // Selected zone has been removed. if (zone_id != null && !zone_rows.has_key(zone_id)) { this.zone = null; foreach (var entry in zone_rows) { this.zone = entry.value.zone; break; if (should_set_zone) { var first = zones.get_item(0); if (first != null) { this.selected = (ZoneWrapper) first; } else { this.selected = null; } } this.render(); } 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; } var item = (ZoneWrapper) zones.get_item(found_position); item.zone = zone; } public PlaybackToolbar() { Object(); } construct { zone_list.row_activated.connect((row) => { this.zone = ((ZoneSelectorRow) row).zone; zone_list_popover.popdown(); var zone_list_factory = new Gtk.SignalListItemFactory(); zone_list_factory.setup.connect((item) => { var label = new Gtk.Label(""); ((Gtk.ListItem) item).child = label; }); 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.notify["selected"].connect(() => { this.selected = (ZoneWrapper) zones.get_item(zone_list.selected); this.render(); }); play.clicked.connect(() => { var row = zone_rows[zone_id]; if (conn == null || row == null) { if (conn == null || zone == null) { return; } this.disable_actions(); conn.control.begin(row.zone, Plac.Transport.ACTION_PLAY, (obj, res) => { conn.control.begin(zone, Plac.Transport.ACTION_PLAY, (obj, res) => { conn.control.end(res); this.enable_actions(); }); }); pause.clicked.connect(() => { var row = zone_rows[zone_id]; if (conn == null || row == null) { if (conn == null || zone == null) { return; } this.disable_actions(); conn.control.begin(row.zone, Plac.Transport.ACTION_PAUSE, (obj, res) => { conn.control.begin(zone, Plac.Transport.ACTION_PAUSE, (obj, res) => { conn.control.end(res); this.enable_actions(); }); }); prev.clicked.connect(() => { var row = zone_rows[zone_id]; if (conn == null || row == null) { if (conn == null || zone == null) { return; } this.disable_actions(); conn.control.begin(row.zone, Plac.Transport.ACTION_PREV, (obj, res) => { conn.control.begin(zone, Plac.Transport.ACTION_PREV, (obj, res) => { conn.control.end(res); this.enable_actions(); }); }); next.clicked.connect(() => { var row = zone_rows[zone_id]; if (conn == null || row == null) { if (conn == null || zone == null) { return; } this.disable_actions(); conn.control.begin(row.zone, Plac.Transport.ACTION_NEXT, (obj, res) => { conn.control.begin(zone, Plac.Transport.ACTION_NEXT, (obj, res) => { conn.control.end(res); this.enable_actions(); });
-
@@ -354,6 +375,65 @@ play.visible = false;pause.visible = true; break; } stale_outputs(); foreach (var output in zone.outputs) { update_output(output); } 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; } row.stale = true; } } 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; } if (row.stale) { rows_to_remove.add(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) { zone_outputs.append(new ZoneOutputRow(output)); return; } if (row.output_id != output.id) { continue; } row.output = output; row.stale = false; return; } } private void disable_actions() {
-
@@ -369,23 +449,17 @@ seek.sensitive = true;} } class ZoneSelectorRow : Gtk.ListBoxRow { private Gtk.Label label; public Plac.Transport.Zone zone { get; construct set; } // 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; } public ZoneSelectorRow(Plac.Transport.Zone zone) { public ZoneWrapper(Plac.Transport.Zone zone) { Object(zone: zone); } construct { label = new Gtk.Label(zone.name); this.child = label; this.activatable = true; } public void render() { this.label.label = zone.name; public static bool is_equal(ZoneWrapper a, ZoneWrapper b) { return a.zone.id == b.zone.id; } } }
-
-
-
@@ -0,0 +1,62 @@// 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/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 value_control; [GtkChild] private unowned Gtk.Scale volume_slider; public bool stale = false; public string output_id { get { return _output.id; } } 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; volume_slider.adjustment.lower = output.volume.min; volume_slider.adjustment.upper = output.volume.max; volume_slider.adjustment.value = output.volume.value; volume_slider.adjustment.step_increment = output.volume.step; } } } public ZoneOutputRow(Plac.Transport.Output output) { Object(output: output); } } }
-