Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
df752940d8
10 changed files with 600 additions and 38 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -899,7 +899,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "build_helpers"
|
name = "build_helpers"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
@ -1393,7 +1393,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config"
|
name = "cosmic-config"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomicwrites",
|
"atomicwrites",
|
||||||
"cosmic-config-derive",
|
"cosmic-config-derive",
|
||||||
|
|
@ -1414,7 +1414,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config-derive"
|
name = "cosmic-config-derive"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
|
|
@ -1642,7 +1642,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-audio-client"
|
name = "cosmic-settings-audio-client"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"cosmic-settings-audio-core",
|
"cosmic-settings-audio-core",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|
@ -1658,7 +1658,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-audio-core"
|
name = "cosmic-settings-audio-core"
|
||||||
version = "1.0.0"
|
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 = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"ron 0.12.2",
|
"ron 0.12.2",
|
||||||
|
|
@ -1681,7 +1681,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-config"
|
name = "cosmic-settings-config"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
"ron 0.11.0",
|
"ron 0.11.0",
|
||||||
|
|
@ -1702,7 +1702,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-settings-daemon-config"
|
name = "cosmic-settings-daemon-config"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2349,7 +2349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3266,7 +3266,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_accessibility"
|
name = "iced_accessibility"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_winit",
|
"accesskit_winit",
|
||||||
|
|
@ -3275,7 +3275,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_core"
|
name = "iced_core"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"build_helpers",
|
"build_helpers",
|
||||||
|
|
@ -3312,7 +3312,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_futures"
|
name = "iced_futures"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#1f6dc991eaa5115a09be2505cc8a323e5b2b0bff"
|
source = "git+https://github.com/pop-os/libcosmic#1f8d8786940cc6db59d412153c5a03f69a20375c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
|
|
@ -6473,7 +6473,7 @@ dependencies = [
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6486,7 +6486,7 @@ dependencies = [
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7209,7 +7209,7 @@ dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8272,7 +8272,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -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
|
- [ ] Output balance slider works & doesn't get reset by volume slider
|
||||||
- [ ] Amplification toggles work as expected
|
- [ ] Amplification toggles work as expected
|
||||||
- [ ] Device profiles can be changed & reflect as expected on the main Sound page
|
- [ ] 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
|
### Power & Battery
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
#[cfg(feature = "page-sound")]
|
||||||
crate::pages::Message::SoundDeviceProfiles(message) => {
|
crate::pages::Message::SoundDeviceProfiles(message) => {
|
||||||
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {
|
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use cosmic::app::ContextDrawer;
|
use cosmic::app::ContextDrawer;
|
||||||
use cosmic::iced::{Alignment, Length};
|
use cosmic::iced::{Alignment, Length};
|
||||||
use cosmic::widget::text_input::focus;
|
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::{Apply, Element, Task, task, widget};
|
||||||
use cosmic_settings_page::section::Entity;
|
use cosmic_settings_page::section::Entity;
|
||||||
use cosmic_settings_page::{self as page, Content, Info, Section};
|
use cosmic_settings_page::{self as page, Content, Info, Section};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use std::sync::{Arc, LazyLock};
|
||||||
use cosmic::config::{CosmicTk, FontConfig};
|
use cosmic::config::{CosmicTk, FontConfig};
|
||||||
use cosmic::{Apply, Element, Task, widget};
|
use cosmic::{Apply, Element, Task, widget};
|
||||||
use cosmic_config::ConfigSet;
|
use cosmic_config::ConfigSet;
|
||||||
use mime::FONT;
|
|
||||||
|
|
||||||
use crate::app;
|
use crate::app;
|
||||||
use crate::widget::selection_context_item;
|
use crate::widget::selection_context_item;
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ pub enum Message {
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
Sound(sound::Message),
|
Sound(sound::Message),
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
|
SoundApplications(sound::applications::Message),
|
||||||
|
#[cfg(feature = "page-sound")]
|
||||||
SoundDeviceProfiles(sound::device_profiles::Message),
|
SoundDeviceProfiles(sound::device_profiles::Message),
|
||||||
StartupApps(applications::startup_apps::Message),
|
StartupApps(applications::startup_apps::Message),
|
||||||
#[cfg(feature = "page-users")]
|
#[cfg(feature = "page-users")]
|
||||||
|
|
|
||||||
465
cosmic-settings/src/pages/sound/applications.rs
Normal file
465
cosmic-settings/src/pages/sound/applications.rs
Normal 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2023 System76 <info@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
pub mod applications;
|
||||||
pub mod device_profiles;
|
pub mod device_profiles;
|
||||||
|
|
||||||
use cosmic::iced::{self, Alignment, Length, window};
|
use cosmic::iced::{self, Alignment, Length, window};
|
||||||
|
|
@ -63,6 +64,7 @@ impl From<Message> for crate::Message {
|
||||||
|
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
entity: page::Entity,
|
entity: page::Entity,
|
||||||
|
applications: page::Entity,
|
||||||
device_profiles: page::Entity,
|
device_profiles: page::Entity,
|
||||||
client: Option<audio_client::Client>,
|
client: Option<audio_client::Client>,
|
||||||
model: model::Model,
|
model: model::Model,
|
||||||
|
|
@ -78,6 +80,7 @@ impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
entity: page::Entity::default(),
|
entity: page::Entity::default(),
|
||||||
|
applications: page::Entity::default(),
|
||||||
device_profiles: page::Entity::default(),
|
device_profiles: page::Entity::default(),
|
||||||
client: None,
|
client: None,
|
||||||
model: model::Model {
|
model: model::Model {
|
||||||
|
|
@ -122,6 +125,7 @@ impl page::Page<crate::pages::Message> for Page {
|
||||||
Some(vec![
|
Some(vec![
|
||||||
sections.insert(output()),
|
sections.insert(output()),
|
||||||
sections.insert(input()),
|
sections.insert(input()),
|
||||||
|
sections.insert(applications()),
|
||||||
sections.insert(device_profiles()),
|
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> {
|
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
||||||
*self = Page {
|
*self = Page {
|
||||||
entity: self.entity,
|
entity: self.entity,
|
||||||
|
applications: self.applications,
|
||||||
device_profiles: self.device_profiles,
|
device_profiles: self.device_profiles,
|
||||||
..Page::default()
|
..Page::default()
|
||||||
};
|
};
|
||||||
|
|
@ -163,8 +168,10 @@ impl page::AutoBind<crate::pages::Message> for Page {
|
||||||
mut page: page::Insert<crate::pages::Message>,
|
mut page: page::Insert<crate::pages::Message>,
|
||||||
) -> page::Insert<crate::pages::Message> {
|
) -> page::Insert<crate::pages::Message> {
|
||||||
let id = page.sub_page_with_id::<device_profiles::Page>();
|
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();
|
let model = page.model.page_mut::<Page>().unwrap();
|
||||||
model.device_profiles = id;
|
model.device_profiles = id;
|
||||||
|
model.applications = applications_id;
|
||||||
page
|
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(
|
||||||
|
§ion.descriptions[button_txt],
|
||||||
|
crate::pages::Message::Page(page.applications),
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// fn alerts() -> Section<crate::pages::Message> {
|
// fn alerts() -> Section<crate::pages::Message> {
|
||||||
// let mut descriptions = Slab::new();
|
// let mut descriptions = Slab::new();
|
||||||
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
|
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
|
||||||
|
|
@ -549,21 +574,3 @@ fn device_profiles() -> Section<crate::pages::Message> {
|
||||||
// .into()
|
// .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(§ion.title)
|
|
||||||
// .add(settings::item(
|
|
||||||
// &*section.descriptions[applications],
|
|
||||||
// text::body("TODO"),
|
|
||||||
// ))
|
|
||||||
// .into()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
1
i18n/en/cosmic_settings.ftl
vendored
1
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -632,6 +632,7 @@ sound-alerts = Alerts
|
||||||
|
|
||||||
sound-applications = Applications
|
sound-applications = Applications
|
||||||
.desc = Application volumes and settings
|
.desc = Application volumes and settings
|
||||||
|
.none = No applications are using sound
|
||||||
|
|
||||||
# No speaker, headphones, or microphone plugged into sound card port
|
# No speaker, headphones, or microphone plugged into sound card port
|
||||||
sound-device-port-unplugged = Unplugged
|
sound-device-port-unplugged = Unplugged
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
use std::sync::Arc;
|
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;
|
use intmap::IntMap;
|
||||||
|
|
||||||
pub type DeviceId = u32;
|
pub type DeviceId = u32;
|
||||||
|
|
@ -18,6 +20,7 @@ pub struct Model {
|
||||||
pub device_profiles_active: IntMap<DeviceId, ProfileInfo>,
|
pub device_profiles_active: IntMap<DeviceId, ProfileInfo>,
|
||||||
pub device_routes: IntMap<DeviceId, Vec<RouteInfo>>,
|
pub device_routes: IntMap<DeviceId, Vec<RouteInfo>>,
|
||||||
pub node_devices: IntMap<NodeId, Option<u32>>,
|
pub node_devices: IntMap<NodeId, Option<u32>>,
|
||||||
|
pub playback: Vec<Playback>,
|
||||||
pub sinks: Nodes,
|
pub sinks: Nodes,
|
||||||
pub sources: Nodes,
|
pub sources: Nodes,
|
||||||
pub active_sink: ActiveNode,
|
pub active_sink: ActiveNode,
|
||||||
|
|
@ -27,6 +30,43 @@ pub struct Model {
|
||||||
pub text: Text,
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
pub hd_audio: String,
|
pub hd_audio: String,
|
||||||
|
|
@ -106,7 +146,9 @@ impl Model {
|
||||||
tracing::info!(target: "sound", ?event, "update");
|
tracing::info!(target: "sound", ?event, "update");
|
||||||
match event {
|
match event {
|
||||||
audio_client::Event::NodeMute(node_id, mute) => {
|
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;
|
self.sinks.mute[pos] = mute;
|
||||||
if self.sinks.active == Some(pos) {
|
if self.sinks.active == Some(pos) {
|
||||||
self.active_sink.mute = mute;
|
self.active_sink.mute = mute;
|
||||||
|
|
@ -120,7 +162,10 @@ impl Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_client::Event::NodeVolume(node_id, volume, balance) => {
|
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.volume[pos] = volume;
|
||||||
self.sinks.balance[pos] = balance;
|
self.sinks.balance[pos] = balance;
|
||||||
if self.default_sink.as_ref().is_some_and(|&id| id == node_id)
|
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) => {
|
audio_client::Event::ActiveRoute(device_id, _index, route) => {
|
||||||
self.update_device_names(device_id, &route);
|
self.update_device_names(device_id, &route);
|
||||||
}
|
}
|
||||||
|
|
@ -338,6 +410,12 @@ impl Model {
|
||||||
|
|
||||||
audio_client::Event::RemoveNode(node_id) => {
|
audio_client::Event::RemoveNode(node_id) => {
|
||||||
self.node_devices.remove(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) {
|
if !self.sinks.remove(node_id) {
|
||||||
self.sources.remove(node_id);
|
self.sources.remove(node_id);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue