Merge remote-tracking branch 'origin/master'

This commit is contained in:
Hosted Weblate 2026-08-18 11:10:16 +02:00
commit df752940d8
No known key found for this signature in database
GPG key ID: A3FAAA06E6569B4C
10 changed files with 600 additions and 38 deletions

30
Cargo.lock generated
View file

@ -899,7 +899,7 @@ dependencies = [
[[package]]
name = "build_helpers"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"cfg_aliases 0.2.1",
]
@ -1393,7 +1393,7 @@ dependencies = [
[[package]]
name = "cosmic-config"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"atomicwrites",
"cosmic-config-derive",
@ -1414,7 +1414,7 @@ dependencies = [
[[package]]
name = "cosmic-config-derive"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"quote",
"syn 2.0.119",
@ -1642,7 +1642,7 @@ dependencies = [
[[package]]
name = "cosmic-settings-audio-client"
version = "1.0.0"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#21a9692b53fcbffa0f18f7d0a12bf0f9d5bd0590"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#e4d2a74f5115091940f6ea09f730863f74f34758"
dependencies = [
"cosmic-settings-audio-core",
"dirs",
@ -1658,7 +1658,7 @@ dependencies = [
[[package]]
name = "cosmic-settings-audio-core"
version = "1.0.0"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#21a9692b53fcbffa0f18f7d0a12bf0f9d5bd0590"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#e4d2a74f5115091940f6ea09f730863f74f34758"
dependencies = [
"dirs",
"ron 0.12.2",
@ -1681,7 +1681,7 @@ dependencies = [
[[package]]
name = "cosmic-settings-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#21a9692b53fcbffa0f18f7d0a12bf0f9d5bd0590"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#e4d2a74f5115091940f6ea09f730863f74f34758"
dependencies = [
"cosmic-config",
"ron 0.11.0",
@ -1702,7 +1702,7 @@ dependencies = [
[[package]]
name = "cosmic-settings-daemon-config"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#21a9692b53fcbffa0f18f7d0a12bf0f9d5bd0590"
source = "git+https://github.com/pop-os/cosmic-settings-daemon#e4d2a74f5115091940f6ea09f730863f74f34758"
dependencies = [
"cosmic-config",
"serde",
@ -2349,7 +2349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -3266,7 +3266,7 @@ dependencies = [
[[package]]
name = "iced_accessibility"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"accesskit",
"accesskit_winit",
@ -3275,7 +3275,7 @@ dependencies = [
[[package]]
name = "iced_core"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"bitflags 2.13.1",
"build_helpers",
@ -3312,7 +3312,7 @@ dependencies = [
[[package]]
name = "iced_futures"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
dependencies = [
"futures",
"iced_core",
@ -6473,7 +6473,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -6486,7 +6486,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -7209,7 +7209,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -8272,7 +8272,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]

View file

@ -52,6 +52,9 @@ This document provides a regression testing checklist for COSMIC Settings and th
- [ ] Output balance slider works & doesn't get reset by volume slider
- [ ] Amplification toggles work as expected
- [ ] Device profiles can be changed & reflect as expected on the main Sound page
- [ ] Applications using sound appear on the Applications sub-page
- [ ] Application volume and mute controls only affect the selected playback stream
- [ ] Closing an playback stream removes it from the Applications sub-page
### Power & Battery

View file

@ -578,6 +578,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::SoundApplications(message) => {
if let Some(page) = self.pages.page_mut::<sound::applications::Page>() {
return page.update(message).map(Into::into);
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::SoundDeviceProfiles(message) => {
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {

View file

@ -1,7 +1,7 @@
use cosmic::app::ContextDrawer;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::text_input::focus;
use cosmic::widget::{Id, button, icon, settings, text};
use cosmic::widget::{button, icon, settings, text};
use cosmic::{Apply, Element, Task, task, widget};
use cosmic_settings_page::section::Entity;
use cosmic_settings_page::{self as page, Content, Info, Section};

View file

@ -6,7 +6,6 @@ use std::sync::{Arc, LazyLock};
use cosmic::config::{CosmicTk, FontConfig};
use cosmic::{Apply, Element, Task, widget};
use cosmic_config::ConfigSet;
use mime::FONT;
use crate::app;
use crate::widget::selection_context_item;

View file

@ -87,6 +87,8 @@ pub enum Message {
#[cfg(feature = "page-sound")]
Sound(sound::Message),
#[cfg(feature = "page-sound")]
SoundApplications(sound::applications::Message),
#[cfg(feature = "page-sound")]
SoundDeviceProfiles(sound::device_profiles::Message),
StartupApps(applications::startup_apps::Message),
#[cfg(feature = "page-users")]

View file

@ -0,0 +1,465 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::model;
use cosmic::desktop::{
DesktopEntryCache, DesktopLookupContext, DesktopResolveOptions, resolve_desktop_entry,
};
use cosmic::iced::futures;
use cosmic::iced::{self, Alignment, Length, window};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::{Apply, Element, surface, widget};
use cosmic_config::{Config, ConfigGet};
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use cosmic_settings_page::{self as page, Section, section};
use futures::executor::block_on;
use intmap::IntMap;
use slotmap::SlotMap;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
const AUDIO_CONFIG: &str = "com.system76.CosmicAudio";
const AMPLIFICATION_SINK: &str = "amplification_sink";
#[derive(Clone, Debug)]
pub enum Message {
Model(cosmic_settings_sound::Message),
SetBalance(u32, f32),
SetMute(u32, bool),
SetSink(u32, usize),
SetVolume(u32, u32),
Surface(surface::Action),
ToggleExpanded(u32),
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::SoundApplications(message)
}
}
impl From<Message> for crate::Message {
fn from(message: Message) -> Self {
crate::Message::PageMessage(message.into())
}
}
#[derive(Default)]
pub struct Page {
entity: page::Entity,
model: model::Model,
client: Option<Rc<RefCell<audio_client::Client>>>,
desktop_entries: DesktopEntryCache,
playback_icons: IntMap<u32, String>,
amplification_sink: bool,
expanded: Option<u32>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
self.amplification_sink = Config::new(AUDIO_CONFIG, 1)
.ok()
.and_then(|config| config.get::<bool>(AMPLIFICATION_SINK).ok())
.unwrap_or(true);
cosmic::Task::none()
}
fn info(&self) -> page::Info {
page::Info::new("sound-applications", "preferences-sound-symbolic")
.title(fl!("sound-applications"))
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(view())])
}
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
*self = Page {
entity: self.entity,
..Page::default()
};
cosmic::Task::none()
}
fn subscription(&self, _core: &cosmic::Core) -> iced::Subscription<crate::pages::Message> {
iced::Subscription::run(|| {
iced::stream::channel(
1,
move |emitter: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
cosmic_settings_sound::subscribe(emitter, |m| Message::Model(m).into()).await
},
)
})
}
}
impl Page {
fn resolve_playback_icon(&mut self, node_id: u32, info: &audio_client::PlaybackInfo) {
let Some(candidate) = [
info.application_id.as_deref(),
info.icon_name.as_deref(),
info.application_name.as_deref(),
]
.into_iter()
.flatten()
.find(|value| !value.is_empty()) else {
return;
};
let mut context = DesktopLookupContext::new(candidate);
if let Some(icon_name) = info.icon_name.as_deref().filter(|value| !value.is_empty()) {
context = context.with_identifier(icon_name);
}
if let Some(application_name) = info
.application_name
.as_deref()
.filter(|value| !value.is_empty())
{
context = context.with_title(application_name);
}
let entry = resolve_desktop_entry(
&mut self.desktop_entries,
&context,
&DesktopResolveOptions::default(),
);
let entry_icon = entry.icon();
let icon_name = entry_icon
.filter(|value| !value.is_empty())
.or(info.icon_name.as_deref())
.or(info.application_id.as_deref())
.unwrap_or("application-default");
self.playback_icons.insert(node_id, icon_name.to_owned());
}
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::app::Message> {
match message {
Message::Model(cosmic_settings_sound::Message::Subscription(message)) => {
match &message {
audio_client::Event::Playback(node_id, info) => {
self.resolve_playback_icon(*node_id, info);
}
audio_client::Event::RemoveNode(node_id) => {
self.playback_icons.remove(*node_id);
}
_ => {}
}
self.model.update(message);
}
Message::Model(cosmic_settings_sound::Message::Client(client)) => {
if let Some(client) = Arc::into_inner(client) {
self.client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model::default();
self.playback_icons = IntMap::new();
}
}
Message::SetMute(node_id, mute) => {
self.model
.update(audio_client::Event::NodeMute(node_id, mute));
if let Some(client) = &self.client {
match block_on(client.borrow_mut().conn.set_node_mute(node_id, mute)) {
Ok(Ok(_)) => {}
Ok(Err(why)) => {
tracing::error!(?why, node_id, mute, "daemon rejected playback mute");
}
Err(why) => {
tracing::error!(?why, node_id, mute, "failed to set playback mute");
}
}
} else {
tracing::warn!(node_id, mute, "audio client is not connected");
}
}
Message::SetBalance(playback_id, balance) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == playback_id)
{
playback.balance = Some(balance);
}
if let Some(client) = &self.client {
match block_on(
client
.borrow_mut()
.conn
.set_node_volume_balance(playback_id, Some(balance)),
) {
Ok(Ok(volume)) => {
self.model.update(audio_client::Event::NodeVolume(
playback_id,
volume.volume,
volume.balance,
));
}
Ok(Err(why)) => {
tracing::error!(
?why,
playback_id,
balance,
"daemon rejected playback balance"
);
}
Err(why) => {
tracing::error!(
?why,
playback_id,
balance,
"failed to set playback balance"
);
}
}
} else {
tracing::warn!(playback_id, balance, "audio client is not connected");
}
}
Message::SetSink(playback_id, sorted_pos) => {
let Some(&sink_pos) = self.model.sinks.sorted_index.get(sorted_pos) else {
return cosmic::Task::none();
};
let Some(&sink_id) = self.model.sinks.id.get(sink_pos as usize) else {
return cosmic::Task::none();
};
if let Some(client) = &self.client {
match block_on(
client
.borrow_mut()
.conn
.set_playback_sink(playback_id, sink_id),
) {
Ok(Ok(())) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == playback_id)
{
playback.sink_id = Some(sink_id);
}
}
Ok(Err(why)) => {
tracing::error!(
?why,
playback_id,
sink_id,
"daemon rejected playback output"
);
}
Err(why) => {
tracing::error!(
?why,
playback_id,
sink_id,
"failed to set playback output"
);
}
}
} else {
tracing::warn!(playback_id, sink_id, "audio client is not connected");
}
}
Message::SetVolume(node_id, volume) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == node_id)
{
playback.volume = volume;
}
if let Some(client) = &self.client {
match block_on(client.borrow_mut().conn.set_node_volume(node_id, volume)) {
Ok(Ok(_)) => {}
Ok(Err(why)) => {
tracing::error!(
?why,
node_id,
volume,
"daemon rejected playback volume"
);
}
Err(why) => {
tracing::error!(?why, node_id, volume, "failed to set playback volume");
}
}
} else {
tracing::warn!(node_id, volume, "audio client is not connected");
}
}
Message::Surface(action) => {
return cosmic::task::message(crate::app::Message::Surface(action));
}
Message::ToggleExpanded(node_id) => {
self.expanded = if self.expanded == Some(node_id) {
None
} else {
Some(node_id)
};
}
}
cosmic::Task::none()
}
}
pub fn view() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |_, page, _section| {
if page.model.playback.is_empty() {
return widget::settings::section()
.add(
widget::text::body(fl!("sound-applications", "none"))
.width(Length::Fill)
.align_x(Alignment::Center),
)
.into();
}
let mut applications = widget::settings::section();
for playback in page.model.playback.iter().cloned() {
let node_id = playback.id;
let mute = playback.mute;
let volume = playback.volume;
let expanded = page.expanded == Some(node_id);
let icon_name = page
.playback_icons
.get(node_id)
.map(String::as_str)
.unwrap_or("application-default");
let slider = if page.amplification_sink {
widget::slider(0..=150, volume, move |volume| {
Message::SetVolume(node_id, volume)
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, volume, move |volume| {
Message::SetVolume(node_id, volume)
})
}
.width(Length::Fill)
.apply(widget::container)
.max_width(250.);
let volume_controls = widget::row::with_capacity(5)
.align_y(Alignment::Center)
.push(
widget::button::icon(widget::icon::from_name(if mute {
"audio-volume-muted-symbolic"
} else {
"audio-volume-high-symbolic"
}))
.on_press(Message::SetMute(node_id, !mute)),
)
.push(
widget::text::body(volume.to_string())
.width(Length::Fixed(22.))
.align_x(Alignment::Center),
)
.push(horizontal_space().width(8.))
.push(slider)
.push(
widget::button::icon(widget::icon::from_name(if expanded {
"go-up-symbolic"
} else {
"go-down-symbolic"
}))
.on_press(Message::ToggleExpanded(node_id)),
)
.apply(Element::from)
.map(crate::pages::Message::from);
applications = applications.add(
widget::settings::item::builder(playback.name().to_owned())
.icon(widget::icon::from_name(icon_name).size(16))
.flex_control(volume_controls)
.align_items(Alignment::Center),
);
if !expanded {
continue;
}
let selected_sink =
playback
.selected_sink(page.model.default_sink)
.and_then(|sink_id| {
let sink_pos = page.model.sinks.id.iter().position(|id| *id == sink_id)?;
page.model
.sinks
.sorted_index
.iter()
.position(|pos| usize::from(*pos) == sink_pos)
});
let output = widget::dropdown::popup_dropdown(
&page.model.sinks.sorted_display,
selected_sink,
move |pos| Message::SetSink(node_id, pos),
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
)
.apply(Element::from)
.map(crate::pages::Message::from);
let balance = widget::row::with_capacity(5)
.align_y(Alignment::Center)
.push(
widget::column::with_capacity(2)
.align_x(Alignment::Center)
.push(widget::text::body(fl!("sound-output", "left")))
.push(horizontal_space().width(22.)),
)
.push(horizontal_space().width(8.))
.push(
widget::slider(0.0..=2.0, playback.balance.unwrap_or(1.0), move |balance| {
Message::SetBalance(node_id, balance)
})
.step(0.01)
.breakpoints(&[1.0]),
)
.push(horizontal_space().width(8.))
.push(
widget::column::with_capacity(2)
.align_x(Alignment::Center)
.push(widget::text::body(fl!("sound-output", "right")))
.push(horizontal_space().width(22.)),
)
.apply(Element::from)
.map(crate::pages::Message::from);
applications = applications
.add(widget::settings::item(
fl!("sound-output", "device"),
output,
))
.add(widget::settings::item(
fl!("sound-output", "balance"),
balance,
));
}
applications.into()
})
}

View file

@ -1,6 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod applications;
pub mod device_profiles;
use cosmic::iced::{self, Alignment, Length, window};
@ -63,6 +64,7 @@ impl From<Message> for crate::Message {
pub struct Page {
entity: page::Entity,
applications: page::Entity,
device_profiles: page::Entity,
client: Option<audio_client::Client>,
model: model::Model,
@ -78,6 +80,7 @@ impl Default for Page {
fn default() -> Self {
Self {
entity: page::Entity::default(),
applications: page::Entity::default(),
device_profiles: page::Entity::default(),
client: None,
model: model::Model {
@ -122,6 +125,7 @@ impl page::Page<crate::pages::Message> for Page {
Some(vec![
sections.insert(output()),
sections.insert(input()),
sections.insert(applications()),
sections.insert(device_profiles()),
])
}
@ -150,6 +154,7 @@ impl page::Page<crate::pages::Message> for Page {
fn on_leave(&mut self) -> Task<crate::pages::Message> {
*self = Page {
entity: self.entity,
applications: self.applications,
device_profiles: self.device_profiles,
..Page::default()
};
@ -163,8 +168,10 @@ impl page::AutoBind<crate::pages::Message> for Page {
mut page: page::Insert<crate::pages::Message>,
) -> page::Insert<crate::pages::Message> {
let id = page.sub_page_with_id::<device_profiles::Page>();
let applications_id = page.sub_page_with_id::<applications::Page>();
let model = page.model.page_mut::<Page>().unwrap();
model.device_profiles = id;
model.applications = applications_id;
page
}
}
@ -534,6 +541,24 @@ fn device_profiles() -> Section<crate::pages::Message> {
})
}
/// A section for opening the application volume controls sub-page.
fn applications() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
button_txt = fl!("sound-applications");
});
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
settings::section()
.add(crate::widget::go_next_item(
&section.descriptions[button_txt],
crate::pages::Message::Page(page.applications),
))
.into()
})
}
// fn alerts() -> Section<crate::pages::Message> {
// let mut descriptions = Slab::new();
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
@ -549,21 +574,3 @@ fn device_profiles() -> Section<crate::pages::Message> {
// .into()
// })
// }
// fn applications() -> Section<crate::pages::Message> {
// let mut descriptions = Slab::new();
// let applications = descriptions.insert(fl!("sound-applications", "desc"));
// Section::default()
// .title(fl!("sound-applications"))
// .descriptions(descriptions)
// .view::<Page>(move |_binder, _page, section| {
// settings::section().title(&section.title)
// .add(settings::item(
// &*section.descriptions[applications],
// text::body("TODO"),
// ))
// .into()
// })
// }

View file

@ -632,6 +632,7 @@ sound-alerts = Alerts
sound-applications = Applications
.desc = Application volumes and settings
.none = No applications are using sound
# No speaker, headphones, or microphone plugged into sound card port
sound-device-port-unplugged = Unplugged

View file

@ -3,7 +3,9 @@
use std::sync::Arc;
use cosmic_settings_audio_client::{self as audio_client, Availability, ProfileInfo, RouteInfo};
use cosmic_settings_audio_client::{
self as audio_client, Availability, PlaybackInfo, ProfileInfo, RouteInfo,
};
use intmap::IntMap;
pub type DeviceId = u32;
@ -18,6 +20,7 @@ pub struct Model {
pub device_profiles_active: IntMap<DeviceId, ProfileInfo>,
pub device_routes: IntMap<DeviceId, Vec<RouteInfo>>,
pub node_devices: IntMap<NodeId, Option<u32>>,
pub playback: Vec<Playback>,
pub sinks: Nodes,
pub sources: Nodes,
pub active_sink: ActiveNode,
@ -27,6 +30,43 @@ pub struct Model {
pub text: Text,
}
#[derive(Clone, Debug)]
pub struct Playback {
pub id: NodeId,
pub info: PlaybackInfo,
pub mute: bool,
pub volume: u32,
pub balance: Option<f32>,
/// Explicitly selected output. `None` means the playback follows the default sink.
pub sink_id: Option<NodeId>,
}
impl Playback {
pub fn name(&self) -> &str {
[
self.info.application_name.as_deref(),
self.info.application_id.as_deref(),
self.info.media_name.as_deref(),
Some(self.info.node_name.as_str()),
]
.into_iter()
.flatten()
.find(|value| !value.is_empty())
.unwrap_or_default()
}
pub fn description(&self) -> Option<&str> {
self.info
.media_name
.as_deref()
.filter(|description| !description.is_empty() && *description != self.name())
}
pub fn selected_sink(&self, default_sink: Option<NodeId>) -> Option<NodeId> {
self.sink_id.or(default_sink)
}
}
#[derive(Debug, Default)]
pub struct Text {
pub hd_audio: String,
@ -106,7 +146,9 @@ impl Model {
tracing::info!(target: "sound", ?event, "update");
match event {
audio_client::Event::NodeMute(node_id, mute) => {
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
if let Some(playback) = self.playback.iter_mut().find(|item| item.id == node_id) {
playback.mute = mute;
} else if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
self.sinks.mute[pos] = mute;
if self.sinks.active == Some(pos) {
self.active_sink.mute = mute;
@ -120,7 +162,10 @@ impl Model {
}
audio_client::Event::NodeVolume(node_id, volume, balance) => {
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
if let Some(playback) = self.playback.iter_mut().find(|item| item.id == node_id) {
playback.volume = volume;
playback.balance = balance;
} else if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
self.sinks.volume[pos] = volume;
self.sinks.balance[pos] = balance;
if self.default_sink.as_ref().is_some_and(|&id| id == node_id)
@ -304,6 +349,33 @@ impl Model {
}
}
audio_client::Event::Playback(node_id, info) => {
if let Some(playback) = self.playback.iter_mut().find(|item| item.id == node_id) {
playback.info = info;
} else {
self.playback.push(Playback {
id: node_id,
info,
mute: false,
volume: 0,
balance: None,
sink_id: None,
});
}
self.playback
.sort_by(|a, b| a.name().cmp(b.name()).then(a.id.cmp(&b.id)));
}
audio_client::Event::PlaybackTarget(playback_id, sink_id) => {
if let Some(playback) = self
.playback
.iter_mut()
.find(|playback| playback.id == playback_id)
{
playback.sink_id = sink_id;
}
}
audio_client::Event::ActiveRoute(device_id, _index, route) => {
self.update_device_names(device_id, &route);
}
@ -338,6 +410,12 @@ impl Model {
audio_client::Event::RemoveNode(node_id) => {
self.node_devices.remove(node_id);
self.playback.retain(|item| item.id != node_id);
for playback in &mut self.playback {
if playback.sink_id == Some(node_id) {
playback.sink_id = None;
}
}
if !self.sinks.remove(node_id) {
self.sources.remove(node_id);