Merge remote-tracking branch 'upstream/master' into local/yoda-main-1.3.0
# Conflicts: # Cargo.lock # Cargo.toml # cosmic-settings/Cargo.toml
This commit is contained in:
commit
02eccba3f8
62 changed files with 2908 additions and 625 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-settings"
|
||||
version = "1.0.12"
|
||||
version = "1.5.0"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
publish = false
|
||||
|
|
@ -16,7 +16,7 @@ ashpd = { version = "0.12", default-features = false, features = [
|
|||
], optional = true }
|
||||
chrono = "0.4"
|
||||
jiff = "0.2"
|
||||
clap = { version = "4.5.59", features = ["derive"] }
|
||||
clap = { version = "4.6.4", features = ["derive"] }
|
||||
color-eyre = "0.6.5"
|
||||
cosmic-bg-config.workspace = true
|
||||
cosmic-comp-config = { workspace = true, optional = true }
|
||||
|
|
@ -41,28 +41,28 @@ derive_setters = "0.1.9"
|
|||
dirs = "6.0.0"
|
||||
eyre = "0.6.12"
|
||||
freedesktop-desktop-entry = "0.8.1"
|
||||
futures = "0.3.32"
|
||||
futures = "0.3.33"
|
||||
hostname-validator = "1.1.1"
|
||||
hostname1-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
i18n-embed-fl = "0.10.0"
|
||||
i18n-embed-fl = "0.10.1"
|
||||
image.workspace = true
|
||||
indexmap = "2.13.0"
|
||||
indexmap = "2.14.0"
|
||||
itertools = "0.14.0"
|
||||
itoa = "1.0.17"
|
||||
itoa = "1.0.18"
|
||||
cosmic.workspace = true
|
||||
iced_winit.workspace = true
|
||||
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
sysinfo = { version = "=0.38.0", optional = true }
|
||||
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
|
||||
notify = "8.2.0"
|
||||
nmrs = { version = "3.4.0", optional = true }
|
||||
regex = "1.12.3"
|
||||
nmrs = { version = "3.4.2", optional = true }
|
||||
regex = "1.13.1"
|
||||
ron = "0.12"
|
||||
rust-embed = "8.11.0"
|
||||
rust-embed = "8.12.0"
|
||||
sctk = { workspace = true, optional = true }
|
||||
secure-string = "0.3.0"
|
||||
secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"], optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
slab = "0.4.12"
|
||||
slotmap = "1.1.1"
|
||||
static_init = "1.0.4"
|
||||
|
|
@ -70,26 +70,26 @@ sunrise = "3.0.0"
|
|||
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
tokio = { workspace = true, features = ["fs", "io-util", "process", "sync"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
udev = { version = "0.9.3", optional = true }
|
||||
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
bluez-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
|
||||
url = "2.5.8"
|
||||
xkb-data = "0.2.2"
|
||||
xkeysym = { version = "0.2.1", optional = true }
|
||||
zbus = { version = "5.13.2", default-features = false, features = [
|
||||
zbus = { version = "5.18.0", default-features = false, features = [
|
||||
"tokio",
|
||||
], optional = true }
|
||||
zbus_polkit = { version = "5.0.0", optional = true }
|
||||
fontdb = "0.23.0"
|
||||
mime = "0.3.17"
|
||||
rustix = { version = "1.1.3", features = ["process"] }
|
||||
rustix = { version = "1.1.4", features = ["process"] }
|
||||
gettext-rs = { version = "0.7.7", features = [
|
||||
"gettext-system",
|
||||
], optional = true }
|
||||
num-traits = "0.2"
|
||||
pwhash = "1"
|
||||
which = "8.0.0"
|
||||
which = "8.0.5"
|
||||
zlink = "0.5.0"
|
||||
intmap = "3.1.3"
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ features = ["codec"]
|
|||
optional = true
|
||||
|
||||
[dependencies.icu]
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
features = ["compiled_data"]
|
||||
|
||||
[dependencies.i18n-embed]
|
||||
|
|
@ -196,6 +196,6 @@ cosmic-comp-config = ["dep:cosmic-comp-config"]
|
|||
dbus-config = ["cosmic/dbus-config", "cosmic-config/dbus"]
|
||||
single-instance = ["cosmic/single-instance"]
|
||||
test = []
|
||||
wayland = ["cosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr", "iced_winit/cctk"]
|
||||
wayland = ["cosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr", "iced_winit/wayland"]
|
||||
wgpu = ["cosmic/wgpu"]
|
||||
xdg-portal = ["ashpd", "cosmic/xdg-portal"]
|
||||
|
|
|
|||
|
|
@ -584,6 +584,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>() {
|
||||
|
|
@ -1158,6 +1165,9 @@ impl SettingsApp {
|
|||
}
|
||||
|
||||
for page in unload {
|
||||
if page == self.active_page {
|
||||
continue;
|
||||
}
|
||||
self.loaded_pages.remove(&page);
|
||||
self.pages.on_leave(page);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -153,6 +153,16 @@ impl Content {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn preserve_from(&mut self, previous: &mut Content) {
|
||||
self.font_config
|
||||
.take_families_from(&mut previous.font_config);
|
||||
|
||||
self.icons_fetched = previous.icons_fetched;
|
||||
self.icon_theme_active = previous.icon_theme_active;
|
||||
self.icon_themes = std::mem::take(&mut previous.icon_themes);
|
||||
self.icon_handles = std::mem::take(&mut previous.icon_handles);
|
||||
}
|
||||
|
||||
pub fn update_font(
|
||||
&mut self,
|
||||
message: FontMessage,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -94,6 +93,11 @@ impl Model {
|
|||
self.font_filter.clear();
|
||||
}
|
||||
|
||||
pub fn take_families_from(&mut self, other: &mut Model) {
|
||||
self.interface_font_families = std::mem::take(&mut other.interface_font_families);
|
||||
self.monospace_font_families = std::mem::take(&mut other.monospace_font_families);
|
||||
}
|
||||
|
||||
pub fn font_loaded(
|
||||
&mut self,
|
||||
mono: Vec<Arc<str>>,
|
||||
|
|
|
|||
|
|
@ -200,7 +200,9 @@ impl Page {
|
|||
|
||||
Message::DrawerOpen(context_view) => {
|
||||
self.context_view = Some(context_view);
|
||||
self.drawer = drawer::Content::from(&self.theme_manager);
|
||||
let mut previous =
|
||||
std::mem::replace(&mut self.drawer, drawer::Content::from(&self.theme_manager));
|
||||
self.drawer.preserve_from(&mut previous);
|
||||
tasks.push(cosmic::task::message(
|
||||
crate::app::Message::OpenContextDrawer(self.entity),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ pub enum Message {
|
|||
AddFolder(Arc<Result<Url, file_chooser::Error>>),
|
||||
/// Adds a new image file the system wallpaper folder.
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
AddFile(Arc<Result<Url, file_chooser::Error>>),
|
||||
AddFiles(Arc<Result<Vec<Url>, file_chooser::Error>>),
|
||||
/// Cache currently displayed image.
|
||||
CacheDisplayImage,
|
||||
/// Selects an option in the category dropdown menu.
|
||||
|
|
@ -787,11 +787,11 @@ impl Page {
|
|||
.title(fl!("wallpaper", "image-dialog"))
|
||||
.accept_label(fl!("dialog-add"))
|
||||
.modal(false)
|
||||
.open_file()
|
||||
.open_files()
|
||||
.await
|
||||
.map(|response| response.url().to_owned());
|
||||
.map(|response| response.urls().to_vec());
|
||||
|
||||
let message = Message::AddFile(Arc::new(dialog_result));
|
||||
let message = Message::AddFiles(Arc::new(dialog_result));
|
||||
let page_message = crate::pages::Message::DesktopWallpaper(message);
|
||||
crate::Message::PageMessage(page_message)
|
||||
});
|
||||
|
|
@ -862,6 +862,11 @@ impl Page {
|
|||
#[cfg(feature = "xdg-portal")]
|
||||
Message::AddFolder(result) => {
|
||||
let path = match dialog_response(result) {
|
||||
DialogResponse::Paths(_) => {
|
||||
// TODO find if there is a use of multi folder addition.
|
||||
tracing::warn!("unexpected multi-path response for folder dialog");
|
||||
return Task::none();
|
||||
}
|
||||
DialogResponse::Path(path) => path,
|
||||
DialogResponse::Error(why) => {
|
||||
tracing::error!(why, "dialog response error");
|
||||
|
|
@ -887,27 +892,30 @@ impl Page {
|
|||
}
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
Message::AddFile(result) => {
|
||||
let path = match dialog_response(result) {
|
||||
DialogResponse::Path(path) => path,
|
||||
Message::AddFiles(result) => {
|
||||
let paths = match dialog_response_multi(result) {
|
||||
DialogResponse::Paths(paths) => paths,
|
||||
DialogResponse::Path(path) => vec![path], // not really but for the sake of consistency..
|
||||
DialogResponse::Error(why) => {
|
||||
tracing::error!(why, "dialog response error");
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
|
||||
if path.is_file() {
|
||||
let tasks = paths.into_iter().filter(|path| path.is_file()).map(|path| {
|
||||
tracing::info!(?path, "opening custom image");
|
||||
|
||||
// Loads a single custom image and its thumbnail for display in the backgrounds view.
|
||||
return cosmic::Task::future(async move {
|
||||
cosmic::Task::future(async move {
|
||||
let result = wallpaper::load_image_with_thumbnail(path);
|
||||
|
||||
let message = Message::ImageAdd(result.map(Arc::new));
|
||||
let page_message = crate::pages::Message::DesktopWallpaper(message);
|
||||
crate::Message::PageMessage(page_message)
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
|
||||
Message::Event(event) => match event {
|
||||
|
|
@ -1417,27 +1425,33 @@ pub fn settings() -> Section<crate::pages::Message> {
|
|||
enum DialogResponse {
|
||||
Error(String),
|
||||
Path(PathBuf),
|
||||
Paths(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
fn unwrap_dialog_response<T>(result: Arc<Result<T, file_chooser::Error>>) -> Result<T, String> {
|
||||
let Some(result) = Arc::into_inner(result) else {
|
||||
return Err(String::from("Arc::into_inner returned None"));
|
||||
};
|
||||
|
||||
result.map_err(|why| {
|
||||
let mut source: &dyn std::error::Error = &why;
|
||||
let mut string = format!("open dialog subscription errored\n cause: {source}");
|
||||
|
||||
while let Some(new_source) = source.source() {
|
||||
string.push_str(&format!("\n cause: {new_source}"));
|
||||
source = new_source;
|
||||
}
|
||||
|
||||
string
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
fn dialog_response(result: Arc<Result<Url, file_chooser::Error>>) -> DialogResponse {
|
||||
let Some(result) = Arc::into_inner(result) else {
|
||||
return DialogResponse::Error(String::from("Arc::into_inner returned None"));
|
||||
};
|
||||
|
||||
let selection = match result {
|
||||
Ok(response) => response,
|
||||
Err(why) => {
|
||||
let mut source: &dyn std::error::Error = &why;
|
||||
let mut string = format!("open dialog subscription errored\n cause: {source}");
|
||||
|
||||
while let Some(new_source) = source.source() {
|
||||
string.push_str(&format!("\n cause: {new_source}"));
|
||||
source = new_source;
|
||||
}
|
||||
|
||||
return DialogResponse::Error(string);
|
||||
}
|
||||
let selection = match unwrap_dialog_response(result) {
|
||||
Ok(selection) => selection,
|
||||
Err(why) => return DialogResponse::Error(why),
|
||||
};
|
||||
|
||||
let Ok(path) = selection.to_file_path() else {
|
||||
|
|
@ -1446,3 +1460,22 @@ fn dialog_response(result: Arc<Result<Url, file_chooser::Error>>) -> DialogRespo
|
|||
|
||||
DialogResponse::Path(path)
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdg-portal")]
|
||||
fn dialog_response_multi(result: Arc<Result<Vec<Url>, file_chooser::Error>>) -> DialogResponse {
|
||||
let selections = match unwrap_dialog_response(result) {
|
||||
Ok(selections) => selections,
|
||||
Err(why) => return DialogResponse::Error(why),
|
||||
};
|
||||
|
||||
let mut paths = Vec::with_capacity(selections.len());
|
||||
for selection in selections {
|
||||
let Ok(path) = selection.to_file_path() else {
|
||||
tracing::warn!("not a valid file path: {}", selection.path());
|
||||
continue;
|
||||
};
|
||||
paths.push(path);
|
||||
}
|
||||
|
||||
DialogResponse::Paths(paths)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,11 +244,13 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
|
|||
if let Some(ref on_placement) = self.on_placement {
|
||||
shell.publish(on_placement(
|
||||
output_key,
|
||||
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS) as i32,
|
||||
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS).round()
|
||||
as i32,
|
||||
((region.y
|
||||
- (state.max_dimensions.1 / VERTICAL_DISPLAY_OVERHEAD)
|
||||
- bounds.y)
|
||||
* UNIT_PIXELS) as i32,
|
||||
* UNIT_PIXELS)
|
||||
.round() as i32,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,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")]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
|
|||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use nmrs::agent::{SecretAgent, SecretAgentFlags, SecretRequest, SecretResponder, SecretSetting};
|
||||
use nmrs::raw::zbus;
|
||||
use nmrs::raw::zvariant::{OwnedObjectPath, OwnedValue, Str, Value};
|
||||
use nmrs::raw::zvariant::{Dict, OwnedObjectPath, OwnedValue, Str, Value};
|
||||
use nmrs::{
|
||||
ActiveConnection, ConnectType, ConnectionOptions, EapOptions, NetworkEvent, NetworkManager,
|
||||
SavedConnection, SettingsSummary, WifiKeyMgmt, WifiSecurity,
|
||||
|
|
@ -514,6 +514,10 @@ pub enum Request {
|
|||
Option<String>,
|
||||
),
|
||||
SetAirplaneMode(bool),
|
||||
SetVpnUsername {
|
||||
uuid: UUID,
|
||||
username: String,
|
||||
},
|
||||
SetWiFi(bool),
|
||||
}
|
||||
|
||||
|
|
@ -720,12 +724,64 @@ async fn handle_request(nm: &NetworkManager, req: Request) -> Event {
|
|||
result.is_ok()
|
||||
}
|
||||
Request::SetAirplaneMode(enabled) => nm.set_airplane_mode(*enabled).await.is_ok(),
|
||||
Request::SetVpnUsername { uuid, username } => {
|
||||
let result = set_vpn_username(nm, uuid, username).await;
|
||||
if let Err(err) = &result {
|
||||
tracing::error!(%err, %uuid, "failed to store VPN username");
|
||||
}
|
||||
result.is_ok()
|
||||
}
|
||||
Request::SetWiFi(enabled) => nm.set_wireless_enabled(*enabled).await.is_ok(),
|
||||
};
|
||||
|
||||
request_response(nm, req, success).await
|
||||
}
|
||||
|
||||
/// Stores a VPN username in `vpn.data`, which is where NetworkManager and the VPN
|
||||
/// plugins read it from.
|
||||
///
|
||||
/// The username must not be sent through the secret agent: openvpn fails to
|
||||
/// reconnect when it receives a `username` key in the VPN secrets dictionary.
|
||||
async fn set_vpn_username(
|
||||
nm: &NetworkManager,
|
||||
uuid: &str,
|
||||
username: &str,
|
||||
) -> Result<(), nmrs::ConnectionError> {
|
||||
let settings = nm.get_saved_connection_raw(uuid).await?;
|
||||
|
||||
// The whole dict is rewritten, so the keys already in it have to be carried over.
|
||||
let mut data = HashMap::<String, String>::new();
|
||||
if let Some(stored) = settings.get("vpn").and_then(|vpn| vpn.get("data"))
|
||||
&& let Ok(dict) = Dict::try_from(stored.clone())
|
||||
{
|
||||
for (key, value) in dict.iter() {
|
||||
if let (Ok(key), Ok(value)) = (Str::try_from(key.clone()), Str::try_from(value.clone()))
|
||||
{
|
||||
data.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data
|
||||
.get("username")
|
||||
.is_some_and(|stored| stored == username)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data.insert("username".to_string(), username.to_string());
|
||||
let data = OwnedValue::try_from(Value::from(data))
|
||||
.map_err(|err| nmrs::ConnectionError::VpnFailed(err.to_string()))?;
|
||||
|
||||
let mut patch = nmrs::SettingsPatch::default();
|
||||
patch.raw_overlay = Some(HashMap::from([(
|
||||
"vpn".to_string(),
|
||||
HashMap::from([("data".to_string(), data)]),
|
||||
)]));
|
||||
|
||||
nm.update_saved_connection(uuid, patch).await
|
||||
}
|
||||
|
||||
async fn request_response(nm: &NetworkManager, req: Request, success: bool) -> Event {
|
||||
Event::RequestResponse {
|
||||
req,
|
||||
|
|
@ -1649,15 +1705,28 @@ pub mod nm_secret_agent {
|
|||
}
|
||||
}
|
||||
SecretSetting::Vpn { .. } => {
|
||||
let secrets = stored
|
||||
// Earlier versions stored the username next to the password. openvpn
|
||||
// fails to reconnect when one is present in the secrets dictionary, so
|
||||
// profiles saved by those versions have to be filtered on read too.
|
||||
let secrets: HashMap<String, String> = stored
|
||||
.into_iter()
|
||||
.filter(|(key, _)| key != "username")
|
||||
.map(|(key, value)| (key, value.unsecure().to_string()))
|
||||
.collect();
|
||||
request
|
||||
.responder
|
||||
.vpn_secrets(secrets)
|
||||
.await
|
||||
.map_err(|err| Error(err.to_string()))?;
|
||||
|
||||
if secrets.is_empty() {
|
||||
request
|
||||
.responder
|
||||
.no_secrets()
|
||||
.await
|
||||
.map_err(|err| Error(err.to_string()))?;
|
||||
} else {
|
||||
request
|
||||
.responder
|
||||
.vpn_secrets(secrets)
|
||||
.await
|
||||
.map_err(|err| Error(err.to_string()))?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let setting_name = setting_name(&request.setting).to_string();
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ impl Page {
|
|||
} = dialog
|
||||
&& let Some(NmState { ref sender, .. }) = self.nm_state
|
||||
{
|
||||
let username_unwrapped = username.clone().unwrap_or_default();
|
||||
let username = username.clone().unwrap_or_default();
|
||||
let sec_tx = self.secret_tx.clone();
|
||||
let nm_sender = sender.clone();
|
||||
return Task::future(async move {
|
||||
|
|
@ -626,10 +626,10 @@ impl Page {
|
|||
.send(nm_secret_agent::Request::SetSecrets {
|
||||
setting_name: "vpn".to_string(),
|
||||
uuid: uuid.to_string(),
|
||||
secrets: HashMap::from_iter([
|
||||
("username".to_string(), username_unwrapped.into()),
|
||||
("password".to_string(), password),
|
||||
]),
|
||||
secrets: HashMap::from_iter([(
|
||||
"password".to_string(),
|
||||
password,
|
||||
)]),
|
||||
applied_tx,
|
||||
})
|
||||
.await
|
||||
|
|
@ -645,6 +645,16 @@ impl Page {
|
|||
tracing::error!(%err, "failed to apply secret");
|
||||
}
|
||||
}
|
||||
|
||||
if !username.is_empty() {
|
||||
_ = nm_sender.unbounded_send(
|
||||
network_manager::Request::SetVpnUsername {
|
||||
uuid: uuid.clone(),
|
||||
username,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_ = nm_sender
|
||||
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
}
|
||||
|
|
@ -668,7 +678,7 @@ impl Page {
|
|||
} = dialog
|
||||
&& let Some(NmState { ref sender, .. }) = self.nm_state
|
||||
{
|
||||
let username_unwrapped = username.unwrap_or_default();
|
||||
let username = username.unwrap_or_default();
|
||||
let sec_tx = self.secret_tx.clone();
|
||||
let nm_sender = sender.clone();
|
||||
return Task::future(async move {
|
||||
|
|
@ -678,10 +688,10 @@ impl Page {
|
|||
.send(nm_secret_agent::Request::SetSecrets {
|
||||
setting_name: "vpn".to_string(),
|
||||
uuid: uuid.to_string(),
|
||||
secrets: HashMap::from_iter([
|
||||
("username".to_string(), username_unwrapped.into()),
|
||||
("password".to_string(), password),
|
||||
]),
|
||||
secrets: HashMap::from_iter([(
|
||||
"password".to_string(),
|
||||
password,
|
||||
)]),
|
||||
applied_tx,
|
||||
})
|
||||
.await;
|
||||
|
|
@ -689,6 +699,16 @@ impl Page {
|
|||
tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx)
|
||||
.await;
|
||||
}
|
||||
|
||||
if !username.is_empty() {
|
||||
_ = nm_sender.unbounded_send(
|
||||
network_manager::Request::SetVpnUsername {
|
||||
uuid: uuid.clone(),
|
||||
username,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
Message::Refresh
|
||||
})
|
||||
|
|
|
|||
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>
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -265,7 +272,7 @@ impl Page {
|
|||
tokio::time::sleep(Duration::from_millis(128)).await;
|
||||
_ = client
|
||||
.conn
|
||||
.set_sink_volume(volume.load(atomic::Ordering::Relaxed))
|
||||
.set_source_volume(volume.load(atomic::Ordering::Relaxed))
|
||||
.await;
|
||||
Message::ReattachClient(Arc::new(client)).into()
|
||||
});
|
||||
|
|
@ -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> {
|
||||
// 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(§ion.title)
|
||||
// .add(settings::item(
|
||||
// &*section.descriptions[applications],
|
||||
// text::body("TODO"),
|
||||
// ))
|
||||
// .into()
|
||||
// })
|
||||
// }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue