From 074bf05180588af2ac930600edf20c2c64b59805 Mon Sep 17 00:00:00 2001 From: Piotr Tomaszewski Date: Thu, 3 Sep 2026 15:35:14 +0200 Subject: [PATCH 1/2] wip: about page + cargo fmt --- Cargo.lock | 32 +- cosmic-ext-connect-applet/Cargo.toml | 1 + .../i18n/en/cosmic_ext_connect_applet.ftl | 1 + .../i18n/pl/cosmic_ext_connect_applet.ftl | 1 + cosmic-ext-connect-applet/src/backend.rs | 154 ++++++++-- .../src/cosmic-ext-connect-sms.rs | 5 +- cosmic-ext-connect-applet/src/lib.rs | 4 +- cosmic-ext-connect-applet/src/main.rs | 168 +++++++---- cosmic-ext-connect-applet/src/messages.rs | 23 +- .../src/plugins/sms/actions.rs | 5 +- .../src/plugins/sms/app.rs | 257 +++++++++-------- .../src/plugins/sms/dbus.rs | 47 ++- .../src/plugins/sms/mod.rs | 2 +- .../src/plugins/sms/utils.rs | 5 +- .../src/plugins/sms/views.rs | 4 +- cosmic-ext-connect-applet/src/portal.rs | 5 +- cosmic-ext-connect-applet/src/settings.rs | 250 ++++++++-------- cosmic-ext-connect-applet/src/theme.rs | 4 +- cosmic-ext-connect-applet/src/ui/popup.rs | 148 ++++++++-- kdeconnect-core/src/contacts.rs | 2 +- kdeconnect-core/src/hidden_conversations.rs | 6 +- kdeconnect-core/src/lib.rs | 62 ++-- kdeconnect-core/src/pairing.rs | 5 +- kdeconnect-core/src/plugin_interface.rs | 27 +- kdeconnect-core/src/plugins/mpris.rs | 12 +- kdeconnect-core/src/plugins/notification.rs | 5 +- kdeconnect-core/src/plugins/run_command.rs | 17 +- kdeconnect-core/src/plugins/sms.rs | 23 +- kdeconnect-core/src/plugins/systemvolume.rs | 18 +- kdeconnect-core/src/plugins/telephony.rs | 9 +- kdeconnect-core/src/protocol.rs | 100 +++++-- kdeconnect-core/src/transport.rs | 128 ++++++-- kdeconnect-dbus-client/src/lib.rs | 39 +-- kdeconnect-service/src/dbus_interface.rs | 86 +++--- kdeconnect-service/src/main.rs | 3 +- kdeconnect-service/src/varlink_server.rs | 273 +++++++++++++----- 36 files changed, 1308 insertions(+), 623 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05e2b32f..cb29896c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "cosmic-ext-connect-applet" -version = "0.9.0" +version = "0.10.0" dependencies = [ "anyhow", "ashpd 0.13.12", @@ -1068,6 +1068,7 @@ dependencies = [ "lazy_static", "libcosmic", "notify-rust", + "open", "percent-encoding", "ron", "rust-embed", @@ -2721,6 +2722,25 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "1.0.18" @@ -3842,6 +3862,16 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634" +dependencies = [ + "is-wsl", + "libc", +] + [[package]] name = "option-ext" version = "0.2.0" diff --git a/cosmic-ext-connect-applet/Cargo.toml b/cosmic-ext-connect-applet/Cargo.toml index e771a447..51e18485 100755 --- a/cosmic-ext-connect-applet/Cargo.toml +++ b/cosmic-ext-connect-applet/Cargo.toml @@ -40,6 +40,7 @@ tracing-appender = { workspace = true } i18n-embed = { version = "0.16.0", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.10.0" rust-embed = "8.12.0" +open = "5.4.3" [[bin]] name = "cosmic-ext-connect-applet" diff --git a/cosmic-ext-connect-applet/i18n/en/cosmic_ext_connect_applet.ftl b/cosmic-ext-connect-applet/i18n/en/cosmic_ext_connect_applet.ftl index 9eb83d0a..f8f45ac7 100644 --- a/cosmic-ext-connect-applet/i18n/en/cosmic_ext_connect_applet.ftl +++ b/cosmic-ext-connect-applet/i18n/en/cosmic_ext_connect_applet.ftl @@ -1,5 +1,6 @@ # Applet header applet-title = Cosmic Ext Connect +applet-description = KDE Connect implementation for the COSMIC desktop applet-settings = Settings # Pairing diff --git a/cosmic-ext-connect-applet/i18n/pl/cosmic_ext_connect_applet.ftl b/cosmic-ext-connect-applet/i18n/pl/cosmic_ext_connect_applet.ftl index edf6965a..b317a8a3 100644 --- a/cosmic-ext-connect-applet/i18n/pl/cosmic_ext_connect_applet.ftl +++ b/cosmic-ext-connect-applet/i18n/pl/cosmic_ext_connect_applet.ftl @@ -1,5 +1,6 @@ # Applet header applet-title = Cosmic Ext Connect +applet-description = Natywna implementacja KDE Connect dla pulpitu COSMIC applet-settings = Ustawienia # Pairing diff --git a/cosmic-ext-connect-applet/src/backend.rs b/cosmic-ext-connect-applet/src/backend.rs index 4a0053d7..7d7ac907 100755 --- a/cosmic-ext-connect-applet/src/backend.rs +++ b/cosmic-ext-connect-applet/src/backend.rs @@ -115,7 +115,10 @@ fn merge_device( has_presenter: false, has_lockdevice: false, has_virtualmonitor: false, - run_commands: existing.as_ref().map(|e| e.run_commands.clone()).unwrap_or_default(), + run_commands: existing + .as_ref() + .map(|e| e.run_commands.clone()) + .unwrap_or_default(), }; cache.insert(id, device.clone()); device @@ -134,7 +137,17 @@ pub async fn fetch_devices() -> Vec { let devices: Vec = reply .devices .into_iter() - .map(|d| merge_device(d.id, d.name, d.device_type, d.is_paired, d.is_reachable, &mounted, &mut cache)) + .map(|d| { + merge_device( + d.id, + d.name, + d.device_type, + d.is_paired, + d.is_reachable, + &mounted, + &mut cache, + ) + }) .collect(); return devices; } @@ -150,7 +163,17 @@ pub async fn fetch_devices() -> Vec { let mut cache = DEVICE_CACHE.lock().await; dbus_devices .into_iter() - .map(|d| merge_device(d.id, d.name, "phone".to_string(), d.is_paired, d.is_reachable, &mounted, &mut cache)) + .map(|d| { + merge_device( + d.id, + d.name, + "phone".to_string(), + d.is_paired, + d.is_reachable, + &mounted, + &mut cache, + ) + }) .collect() } Err(e) => { @@ -171,7 +194,11 @@ pub async fn pair_device(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.pair_device(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).pair_device(&device_id).await } @@ -183,7 +210,11 @@ pub async fn unpair_device(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.unpair_device(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).unpair_device(&device_id).await } @@ -193,11 +224,20 @@ pub async fn ping_device(device_id: String) -> Result<()> { let id = device_id.clone(); async move { use kdeconnect_varlink::iface::VarlinkClientInterface; - c.send_ping(id, "Ping from COSMIC!".into()).call().await.map(|_| ()) + c.send_ping(id, "Ping from COSMIC!".into()) + .call() + .await + .map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; - dbus_client!(g).send_ping(&device_id, "Ping from COSMIC!").await + dbus_client!(g) + .send_ping(&device_id, "Ping from COSMIC!") + .await } pub async fn send_files(device_id: String, files: Vec) -> Result<()> { @@ -208,7 +248,11 @@ pub async fn send_files(device_id: String, files: Vec) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.send_files(id, f).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).send_files(&device_id, files).await } @@ -221,7 +265,11 @@ pub async fn share_clipboard(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.share_clipboard(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).share_clipboard(&device_id).await } @@ -233,7 +281,11 @@ pub async fn browse_device_filesystem(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.browse_device(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).browse_device(&device_id).await } @@ -246,7 +298,11 @@ pub async fn unmount_device(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.unmount_device(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).unmount_device(&device_id).await } @@ -257,7 +313,9 @@ pub async fn mounted_devices() -> Vec { if let Some(Ok(reply)) = via_varlink(|c| async move { use kdeconnect_varlink::iface::VarlinkClientInterface; c.mounted_devices().call().await - }).await { + }) + .await + { return reply.device_ids; } let g = CLIENT.lock().await; @@ -274,7 +332,11 @@ pub async fn accept_pairing(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.accept_pairing(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).accept_pairing(&device_id).await } @@ -286,7 +348,11 @@ pub async fn reject_pairing(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.reject_pairing(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).reject_pairing(&device_id).await } @@ -298,7 +364,11 @@ pub async fn ring_device(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.ring_device(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).ring_device(&device_id).await } @@ -309,11 +379,20 @@ pub async fn set_plugin_enabled(device_id: String, plugin_id: String, enabled: b let plug = plugin_id.clone(); async move { use kdeconnect_varlink::iface::VarlinkClientInterface; - c.set_plugin_enabled(id, plug, enabled).call().await.map(|_| ()) + c.set_plugin_enabled(id, plug, enabled) + .call() + .await + .map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; - dbus_client!(g).set_plugin_enabled(&device_id, &plugin_id, enabled).await + dbus_client!(g) + .set_plugin_enabled(&device_id, &plugin_id, enabled) + .await } pub async fn get_disabled_plugins(device_id: String) -> Vec { @@ -323,7 +402,9 @@ pub async fn get_disabled_plugins(device_id: String) -> Vec { use kdeconnect_varlink::iface::VarlinkClientInterface; c.get_disabled_plugins(id).call().await } - }).await { + }) + .await + { return reply.plugins; } @@ -354,7 +435,9 @@ pub async fn has_unread_sms(device_id: String) -> bool { use kdeconnect_varlink::iface::VarlinkClientInterface; c.get_cached_sms(id).call().await } - }).await { + }) + .await + { Some(reply.json) } else { let g = CLIENT.lock().await; @@ -387,7 +470,11 @@ pub async fn broadcast_identity() -> Result<()> { if let Some(r) = via_varlink(|c| async move { use kdeconnect_varlink::iface::VarlinkClientInterface; c.broadcast_identity().call().await.map(|_| ()) - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).broadcast_identity().await } @@ -399,7 +486,11 @@ pub async fn request_run_commands(device_id: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.request_run_commands(id).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).request_run_commands(&device_id).await } @@ -412,7 +503,11 @@ pub async fn execute_run_command(device_id: String, key: String) -> Result<()> { use kdeconnect_varlink::iface::VarlinkClientInterface; c.run_command(id, k).call().await.map(|_| ()) } - }).await { return r; } + }) + .await + { + return r; + } let g = CLIENT.lock().await; dbus_client!(g).run_command(&device_id, &key).await } @@ -585,7 +680,10 @@ async fn session_bus() -> Option<&'static zbus::Connection> { .ok() } -#[zbus::proxy(interface = "org.mpris.MediaPlayer2", default_path = "/org/mpris/MediaPlayer2")] +#[zbus::proxy( + interface = "org.mpris.MediaPlayer2", + default_path = "/org/mpris/MediaPlayer2" +)] trait MprisRoot { #[zbus(property)] fn identity(&self) -> zbus::Result; @@ -658,7 +756,11 @@ async fn read_now_playing(connection: &zbus::Connection, bus_name: &str) -> Opti let art_path = metadata .get("mpris:artUrl") .and_then(|v| String::try_from(v.clone()).ok()) - .map(|uri| uri.strip_prefix("file://").map(str::to_string).unwrap_or(uri)); + .map(|uri| { + uri.strip_prefix("file://") + .map(str::to_string) + .unwrap_or(uri) + }); Some(NowPlaying { identity, diff --git a/cosmic-ext-connect-applet/src/cosmic-ext-connect-sms.rs b/cosmic-ext-connect-applet/src/cosmic-ext-connect-sms.rs index 22383bed..54b773ff 100755 --- a/cosmic-ext-connect-applet/src/cosmic-ext-connect-sms.rs +++ b/cosmic-ext-connect-applet/src/cosmic-ext-connect-sms.rs @@ -23,7 +23,10 @@ fn main() -> cosmic::iced::Result { .cloned() .unwrap_or_else(|| "Unknown Device".to_string()); - info!("KDE Connect SMS window starting: {} ({})", device_name, device_id); + info!( + "KDE Connect SMS window starting: {} ({})", + device_name, device_id + ); cosmic_ext_connect_applet::plugins::sms::run(device_id, device_name) } diff --git a/cosmic-ext-connect-applet/src/lib.rs b/cosmic-ext-connect-applet/src/lib.rs index c8337fb0..69be4c25 100755 --- a/cosmic-ext-connect-applet/src/lib.rs +++ b/cosmic-ext-connect-applet/src/lib.rs @@ -4,8 +4,8 @@ //! settings window, and SMS window binaries. use i18n_embed::{ - fluent::{fluent_language_loader, FluentLanguageLoader}, DesktopLanguageRequester, + fluent::{FluentLanguageLoader, fluent_language_loader}, }; use rust_embed::RustEmbed; @@ -36,5 +36,5 @@ pub mod messages; pub mod models; pub mod plugins; pub mod portal; -pub mod ui; pub mod theme; +pub mod ui; diff --git a/cosmic-ext-connect-applet/src/main.rs b/cosmic-ext-connect-applet/src/main.rs index c3b79a53..9395d7f7 100755 --- a/cosmic-ext-connect-applet/src/main.rs +++ b/cosmic-ext-connect-applet/src/main.rs @@ -9,14 +9,19 @@ use models::{Device, NowPlaying}; use cosmic::app::Core; use cosmic::iced::window::Id as SurfaceId; use cosmic::iced::{Limits, Subscription}; -use cosmic::{Element, Task, widget}; use cosmic::surface::action::{app_popup, destroy_popup}; +use cosmic::{Element, Task, widget}; use std::collections::HashMap; use tracing::{debug, error, info, warn}; +const REPOSITORY_LINK: &str = "https://github.com/cosmic-utils/kdeconnect"; +const SUPPORT_LINK: &str = "https://github.com/cosmic-utils/kdeconnect/issues"; +const LICENSE_LINK: &str = "https://github.com/cosmic-utils/kdeconnect?tab=GPL-3.0-1-ov-file"; + pub struct KdeConnectApplet { core: Core, popup: Option, + page: messages::Page, devices: HashMap, expanded_device: Option, /// Pending pairing requests: device_id → device_name @@ -46,9 +51,9 @@ impl cosmic::Application for KdeConnectApplet { } fn on_close_requested(&self, id: cosmic::iced::window::Id) -> Option { - Some(Message::PopupClosed(id)) + Some(Message::PopupClosed(id)) } - + fn init(core: Core, _flags: Self::Flags) -> (Self, Task>) { tokio::spawn(async { if let Err(e) = backend::initialize().await { @@ -59,6 +64,7 @@ impl cosmic::Application for KdeConnectApplet { let app = KdeConnectApplet { core, popup: None, + page: messages::Page::default(), devices: HashMap::new(), expanded_device: None, pairing_requests: HashMap::new(), @@ -69,38 +75,38 @@ impl cosmic::Application for KdeConnectApplet { (app, Task::none()) } - + fn update(&mut self, message: Self::Message) -> Task> { - match message { + match message { Message::Noop => {} Message::TogglePopup => { return if let Some(p) = self.popup.take() { cosmic::surface::surface_task(destroy_popup(p)) } else { - let show_popup = cosmic::surface::surface_task(app_popup( - |_| Default::default(), - |app: &mut KdeConnectApplet| { - let new_id = cosmic::iced::window::Id::unique(); - - app.popup.replace(new_id); - - let mut popup_settings = app.core.applet.get_popup_settings( - app.core.main_window_id().unwrap(), - new_id, - None, - None, - None, - ); - popup_settings.positioner.size_limits = Limits::NONE - .max_width(400.0) - .min_width(300.0) - .min_height(200.0) - .max_height(600.0); - popup_settings - }, - None, - )); - + let show_popup = cosmic::surface::surface_task(app_popup( + |_| Default::default(), + |app: &mut KdeConnectApplet| { + let new_id = cosmic::iced::window::Id::unique(); + + app.popup.replace(new_id); + + let mut popup_settings = app.core.applet.get_popup_settings( + app.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); + popup_settings.positioner.size_limits = Limits::NONE + .max_width(400.0) + .min_width(300.0) + .min_height(200.0) + .max_height(600.0); + popup_settings + }, + None, + )); + // Fetch devices right away — the polling subscriptions // only run while the popup is open, and their first tick // is a full interval away. The unread-SMS check follows @@ -111,13 +117,26 @@ impl cosmic::Application for KdeConnectApplet { cosmic::Action::App(Message::DevicesUpdated(devices)) }), ]) + }; + } + Message::PopupClosed(id) => { + if self.popup.as_ref() == Some(&id) { + self.popup = None; } } - Message::PopupClosed(id) => { - if self.popup.as_ref() == Some(&id) { - self.popup = None; - } - } + Message::SwitchPage(page) => { + self.page = page; + } + // About Page links opening + Message::OpenRepository => { + let _ = open::that(REPOSITORY_LINK); + } + Message::OpenSupport => { + let _ = open::that(SUPPORT_LINK); + } + Message::OpenLicense => { + let _ = open::that(LICENSE_LINK); + } Message::RefreshDevices => { // The unread-SMS check follows from the resulting // DevicesUpdated, against the fresh device list. @@ -156,7 +175,9 @@ impl cosmic::Application for KdeConnectApplet { self.expanded_device = Some(device_id.clone()); let id = device_id.clone(); return Task::perform( - async move { backend::request_run_commands(id).await.ok(); }, + async move { + backend::request_run_commands(id).await.ok(); + }, |_| cosmic::Action::App(Message::RefreshDevices), ); } @@ -217,13 +238,12 @@ impl cosmic::Application for KdeConnectApplet { } Message::UnmountDevice(ref device_id) => { let id = device_id.clone(); - return Task::perform( - async move { backend::unmount_device(id).await }, - |result| match result { + return Task::perform(async move { backend::unmount_device(id).await }, |result| { + match result { Ok(()) => cosmic::Action::App(Message::RefreshDevices), Err(e) => cosmic::Action::App(Message::BrowseDeviceFailed(e.to_string())), - }, - ); + } + }); } Message::BrowseDeviceFailed(message) => { self.error_banner = Some(message); @@ -274,7 +294,11 @@ impl cosmic::Application for KdeConnectApplet { let id = device_id.clone(); let result_device_id = id.clone(); return Task::perform( - async move { backend::share_clipboard(id).await.map_err(|e| e.to_string()) }, + async move { + backend::share_clipboard(id) + .await + .map_err(|e| e.to_string()) + }, move |result| { cosmic::Action::App(Message::ClipboardSendFinished { device_id: result_device_id.clone(), @@ -293,14 +317,18 @@ impl cosmic::Application for KdeConnectApplet { device.is_charging = Some(charging); // Also patch the backend cache so the next fetch_devices() preserves it let d = device.clone(); - tokio::spawn(async move { backend::update_device(device_id, d).await; }); + tokio::spawn(async move { + backend::update_device(device_id, d).await; + }); } } Message::ConnectivityUpdated(device_id, strength) => { if let Some(device) = self.devices.get_mut(&device_id) { device.signal_strength = Some(strength); let d = device.clone(); - tokio::spawn(async move { backend::update_device(device_id, d).await; }); + tokio::spawn(async move { + backend::update_device(device_id, d).await; + }); } } Message::AcceptPairing(ref device_id) => { @@ -324,7 +352,10 @@ impl cosmic::Application for KdeConnectApplet { ); } Message::PairingRequestReceived(device_id, device_name) => { - info!("Pairing request received from {} ({})", device_name, device_id); + info!( + "Pairing request received from {} ({})", + device_name, device_id + ); self.pairing_requests.insert(device_id, device_name.clone()); // Show a system notification so the user is alerted even if they @@ -396,7 +427,9 @@ impl cosmic::Application for KdeConnectApplet { Message::RequestRunCommands(ref device_id) => { let id = device_id.clone(); return Task::perform( - async move { backend::request_run_commands(id).await.ok(); }, + async move { + backend::request_run_commands(id).await.ok(); + }, |_| cosmic::Action::App(Message::RefreshDevices), ); } @@ -415,14 +448,18 @@ impl cosmic::Application for KdeConnectApplet { device.run_commands = commands; let d = device.clone(); let did = device_id.clone(); - tokio::spawn(async move { backend::update_device(did, d).await; }); + tokio::spawn(async move { + backend::update_device(did, d).await; + }); } } Message::ExecuteRunCommand(ref device_id, ref key) => { let id = device_id.clone(); let k = key.clone(); return Task::perform( - async move { backend::execute_run_command(id, k).await.ok(); }, + async move { + backend::execute_run_command(id, k).await.ok(); + }, |_| cosmic::Action::App(Message::RefreshDevices), ); } @@ -445,15 +482,21 @@ impl cosmic::Application for KdeConnectApplet { if id != popup_id { return widget::text("").into(); } - ui::popup::create_popup_view( - &self.core, - &self.devices, - self.expanded_device.as_ref(), - Some(&self.pairing_requests), - &self.unread_sms, - self.error_banner.as_ref(), - &self.now_playing, - ) + + match &self.page { + messages::Page::Dashboard => { + return ui::popup::create_popup_view( + &self.core, + &self.devices, + self.expanded_device.as_ref(), + Some(&self.pairing_requests), + &self.unread_sms, + self.error_banner.as_ref(), + &self.now_playing, + ); + } + messages::Page::About => return ui::popup::about_view(&self.core), + } } fn style(&self) -> Option { @@ -537,8 +580,7 @@ fn main() -> cosmic::iced::Result { if std::env::var("KDECONNECT_LOG_FILE").is_ok_and(|v| !v.is_empty()) && std::path::Path::new("/.flatpak-info").exists() { - let log_dir = dirs::data_dir() - .unwrap_or_else(|| std::path::PathBuf::from("/tmp")); + let log_dir = dirs::data_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); let _ = std::fs::create_dir_all(&log_dir); let file = std::fs::OpenOptions::new() .create(true) @@ -576,8 +618,14 @@ fn main() -> cosmic::iced::Result { }); let _ = std::process::Command::new("kdeconnect-service") .env("HOME", &home) - .env("XDG_RUNTIME_DIR", std::env::var("XDG_RUNTIME_DIR").unwrap_or_default()) - .env("XDG_CONFIG_HOME", std::env::var("XDG_CONFIG_HOME").unwrap_or_default()) + .env( + "XDG_RUNTIME_DIR", + std::env::var("XDG_RUNTIME_DIR").unwrap_or_default(), + ) + .env( + "XDG_CONFIG_HOME", + std::env::var("XDG_CONFIG_HOME").unwrap_or_default(), + ) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) diff --git a/cosmic-ext-connect-applet/src/messages.rs b/cosmic-ext-connect-applet/src/messages.rs index d3cbbe74..ae7ca23f 100755 --- a/cosmic-ext-connect-applet/src/messages.rs +++ b/cosmic-ext-connect-applet/src/messages.rs @@ -8,10 +8,16 @@ pub enum Message { TogglePopup, Noop, PopupClosed(cosmic::iced::window::Id), + SwitchPage(Page), RefreshDevices, DevicesUpdated(Vec), ToggleDeviceMenu(String), + // From About page links open + OpenRepository, + OpenSupport, + OpenLicense, + // Device actions PingDevice(String), PairDevice(String), @@ -34,8 +40,8 @@ pub enum Message { }, // Battery and connectivity updates — patch device in place without full refresh - BatteryUpdated(String, i32, bool), // device_id, level, is_charging - ConnectivityUpdated(String, i32), // device_id, signal_strength + BatteryUpdated(String, i32, bool), // device_id, level, is_charging + ConnectivityUpdated(String, i32), // device_id, signal_strength // Advanced features RemoteInput(String), @@ -62,10 +68,17 @@ pub enum Message { MprisPrevious(String), // bus_name // Run Command - RequestRunCommands(String), // device_id - RunCommandsReceived(String, String), // device_id, commands_json - ExecuteRunCommand(String, String), // device_id, key + RequestRunCommands(String), // device_id + RunCommandsReceived(String, String), // device_id, commands_json + ExecuteRunCommand(String, String), // device_id, key // SMS unread indicator for the quick-actions menu — device_id -> has_unread UnreadSmsUpdated(HashMap), } + +#[derive(Debug, Clone, Default)] +pub enum Page { + #[default] + Dashboard, + About, +} diff --git a/cosmic-ext-connect-applet/src/plugins/sms/actions.rs b/cosmic-ext-connect-applet/src/plugins/sms/actions.rs index 2ded2f64..288e5850 100644 --- a/cosmic-ext-connect-applet/src/plugins/sms/actions.rs +++ b/cosmic-ext-connect-applet/src/plugins/sms/actions.rs @@ -56,7 +56,10 @@ pub enum SmsMessage { ConfirmDeleteConversation, /// User tapped a thumbnail that hasn't been fully downloaded yet. - RequestFullAttachment { part_id: i64, unique_identifier: String }, + RequestFullAttachment { + part_id: i64, + unique_identifier: String, + }, /// A full-resolution attachment finished downloading. Payload is /// (filename/unique_identifier, saved path) — see /// `kdeconnect_dbus_client::ServiceEvent::SmsAttachmentReceived`. diff --git a/cosmic-ext-connect-applet/src/plugins/sms/app.rs b/cosmic-ext-connect-applet/src/plugins/sms/app.rs index a9eb2895..12c9f6c3 100755 --- a/cosmic-ext-connect-applet/src/plugins/sms/app.rs +++ b/cosmic-ext-connect-applet/src/plugins/sms/app.rs @@ -1,11 +1,11 @@ use async_stream::stream; +use cosmic::iced::widget::scrollable; use cosmic::{ Action, Application, ApplicationExt, Element, Task, app::Core, iced::{Length, Subscription}, widget, }; -use cosmic::iced::widget::scrollable; use futures::StreamExt; use std::collections::HashMap; use tracing::{debug, error, info, warn}; @@ -81,7 +81,7 @@ impl Application for SmsWindow { contacts: Vec::new(), contact_photos: HashMap::new(), selected_thread: None, - contact_idx: Some(0), + contact_idx: Some(0), messages: Vec::new(), message_input: String::new(), search_query: String::new(), @@ -98,10 +98,7 @@ impl Application for SmsWindow { let title = fl!("sms-window-title", device = device_name.as_str()); app.core.window.header_title = title.clone().into(); - let title_task = app.set_window_title( - title, - app.core.main_window_id().unwrap(), - ); + let title_task = app.set_window_title(title, app.core.main_window_id().unwrap()); (app, title_task) } @@ -123,108 +120,108 @@ impl Application for SmsWindow { cosmic::iced::time::every(std::time::Duration::from_secs(45)) .map(|_| SmsMessage::LoadConversations), Subscription::run_with(device_id, |device_id| { - let device_id = device_id.clone(); - stream! { - info!("SMS event stream started for device={}", device_id); - - if let Err(e) = dbus::initialize().await { - error!("SMS D-Bus init failed: {:?}", e); - std::future::pending::<()>().await; - return; - } - - let Some(client) = dbus::get_client().await else { - warn!("SMS D-Bus no client available, stream idle"); - std::future::pending::<()>().await; - return; - }; + let device_id = device_id.clone(); + stream! { + info!("SMS event stream started for device={}", device_id); + + if let Err(e) = dbus::initialize().await { + error!("SMS D-Bus init failed: {:?}", e); + std::future::pending::<()>().await; + return; + } - debug!("SMS event loop entering"); + let Some(client) = dbus::get_client().await else { + warn!("SMS D-Bus no client available, stream idle"); + std::future::pending::<()>().await; + return; + }; - let cached_contacts = dbus::get_cached_contacts(&device_id).await; - if !cached_contacts.is_empty() { - debug!("yielding {} cached contacts at startup", cached_contacts.len()); - yield SmsMessage::ContactsLoaded(cached_contacts); - } + debug!("SMS event loop entering"); - let cached_photos = dbus::get_cached_contact_photos(&device_id).await; - if !cached_photos.is_empty() { - debug!("yielding {} cached contact photos at startup", cached_photos.len()); - yield SmsMessage::ContactPhotosLoaded(cached_photos); - } + let cached_contacts = dbus::get_cached_contacts(&device_id).await; + if !cached_contacts.is_empty() { + debug!("yielding {} cached contacts at startup", cached_contacts.len()); + yield SmsMessage::ContactsLoaded(cached_contacts); + } - if let Some(cached_json) = dbus::get_cached_sms(&device_id).await { - debug!("yielding cached SMS at startup"); - let (messages, conversations) = dbus::parse_sms_messages(&cached_json); - for msg in messages { - yield SmsMessage::ProtocolEventReceived(ProtocolEvent::MessageReceived(msg)); + let cached_photos = dbus::get_cached_contact_photos(&device_id).await; + if !cached_photos.is_empty() { + debug!("yielding {} cached contact photos at startup", cached_photos.len()); + yield SmsMessage::ContactPhotosLoaded(cached_photos); } - yield SmsMessage::ProtocolEventReceived(ProtocolEvent::ConversationsReceived(conversations)); - } - loop { - debug!("SMS subscribing to events"); - let mut event_stream = match client.listen_for_events().await { - Ok(s) => s, - Err(e) => { - warn!("Failed to subscribe to SMS event stream: {:?}", e); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - continue; + if let Some(cached_json) = dbus::get_cached_sms(&device_id).await { + debug!("yielding cached SMS at startup"); + let (messages, conversations) = dbus::parse_sms_messages(&cached_json); + for msg in messages { + yield SmsMessage::ProtocolEventReceived(ProtocolEvent::MessageReceived(msg)); } - }; + yield SmsMessage::ProtocolEventReceived(ProtocolEvent::ConversationsReceived(conversations)); + } - // Subscribe FIRST, then request — contacts response is a - // fire-and-forget D-Bus signal; if we request before subscribing - // the signal arrives while nobody is listening and is lost. - tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; - dbus::fetch_conversations(&device_id).await; - dbus::fetch_contacts(&device_id).await; - - while let Some(event) = event_stream.next().await { - use kdeconnect_dbus_client::ServiceEvent; - match event { - ServiceEvent::SmsMessagesReceived(json) => { - debug!("SmsMessagesReceived len={}", json.len()); - let (messages, conversations) = dbus::parse_sms_messages(&json); - for msg in messages { + loop { + debug!("SMS subscribing to events"); + let mut event_stream = match client.listen_for_events().await { + Ok(s) => s, + Err(e) => { + warn!("Failed to subscribe to SMS event stream: {:?}", e); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + continue; + } + }; + + // Subscribe FIRST, then request — contacts response is a + // fire-and-forget D-Bus signal; if we request before subscribing + // the signal arrives while nobody is listening and is lost. + tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + dbus::fetch_conversations(&device_id).await; + dbus::fetch_contacts(&device_id).await; + + while let Some(event) = event_stream.next().await { + use kdeconnect_dbus_client::ServiceEvent; + match event { + ServiceEvent::SmsMessagesReceived(json) => { + debug!("SmsMessagesReceived len={}", json.len()); + let (messages, conversations) = dbus::parse_sms_messages(&json); + for msg in messages { + yield SmsMessage::ProtocolEventReceived( + ProtocolEvent::MessageReceived(msg) + ); + } yield SmsMessage::ProtocolEventReceived( - ProtocolEvent::MessageReceived(msg) + ProtocolEvent::ConversationsReceived(conversations) ); } - yield SmsMessage::ProtocolEventReceived( - ProtocolEvent::ConversationsReceived(conversations) - ); - } - ServiceEvent::ContactsReceived(contacts) => { - debug!("ContactsReceived {} entries", contacts.len()); - yield SmsMessage::ContactsLoaded(contacts); - } - ServiceEvent::SmsAttachmentReceived(filename, path) => { - debug!("SmsAttachmentReceived {} -> {}", filename, path); - yield SmsMessage::AttachmentReceived(filename, path.into()); - } - ServiceEvent::ContactPhotosReceived(photos) => { - debug!("ContactPhotosReceived {} entries", photos.len()); - let decoded: HashMap> = photos - .into_iter() - .filter_map(|(phone, b64)| { - kdeconnect_core::contacts::decode_photo(&b64) - .map(|bytes| (phone, bytes)) - }) - .collect(); - if !decoded.is_empty() { - yield SmsMessage::ContactPhotosLoaded(decoded); + ServiceEvent::ContactsReceived(contacts) => { + debug!("ContactsReceived {} entries", contacts.len()); + yield SmsMessage::ContactsLoaded(contacts); + } + ServiceEvent::SmsAttachmentReceived(filename, path) => { + debug!("SmsAttachmentReceived {} -> {}", filename, path); + yield SmsMessage::AttachmentReceived(filename, path.into()); } + ServiceEvent::ContactPhotosReceived(photos) => { + debug!("ContactPhotosReceived {} entries", photos.len()); + let decoded: HashMap> = photos + .into_iter() + .filter_map(|(phone, b64)| { + kdeconnect_core::contacts::decode_photo(&b64) + .map(|bytes| (phone, bytes)) + }) + .collect(); + if !decoded.is_empty() { + yield SmsMessage::ContactPhotosLoaded(decoded); + } + } + _ => {} } - _ => {} } - } - warn!("SMS event stream ended, reconnecting in 1s"); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + warn!("SMS event stream ended, reconnecting in 1s"); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } } - } - }), + }), ]) } @@ -244,12 +241,15 @@ impl Application for SmsWindow { } SmsMessage::ContactsLoaded(contacts) => { debug!("ContactsLoaded: {} contacts", contacts.len()); - let sorted = utils::sort_cached_contacts(contacts); + let sorted = utils::sort_cached_contacts(contacts); self.contacts = sorted; self.update_conversation_names(); } SmsMessage::ContactPhotosLoaded(photos) => { - debug!("ContactPhotosLoaded: {} photos, baking off the UI thread", photos.len()); + debug!( + "ContactPhotosLoaded: {} photos, baking off the UI thread", + photos.len() + ); return cosmic::task::future(async move { let baked = tokio::task::spawn_blocking(move || { photos @@ -271,7 +271,10 @@ impl Application for SmsWindow { debug!("AvatarsBaked: {} avatars", baked.len()); self.contact_photos.extend(baked); } - SmsMessage::RequestFullAttachment { part_id, unique_identifier } => { + SmsMessage::RequestFullAttachment { + part_id, + unique_identifier, + } => { debug!("RequestFullAttachment part_id={}", part_id); let device_id = self.device_id.clone(); return cosmic::task::future(async move { @@ -307,7 +310,8 @@ impl Application for SmsWindow { .unwrap_or_else(|| "attachment".to_string()); let Some(dest) = - crate::portal::save_file(fl!("sms-save-attachment-title"), suggested_name).await + crate::portal::save_file(fl!("sms-save-attachment-title"), suggested_name) + .await else { debug!("save attachment cancelled"); return Action::None; @@ -343,8 +347,8 @@ impl Application for SmsWindow { } SmsMessage::PickAttachment => { return cosmic::task::future(async move { - let paths = crate::portal::pick_files(fl!("sms-attach-picker-title"), true, None) - .await; + let paths = + crate::portal::pick_files(fl!("sms-attach-picker-title"), true, None).await; Action::App(SmsMessage::AttachmentsPicked(paths)) }); } @@ -360,7 +364,8 @@ impl Application for SmsWindow { SmsMessage::SelectThread(thread_id) => { debug!("SelectThread: {}", thread_id); if let Some(conv) = self.conversations.iter().find(|c| c.thread_id == thread_id) { - self.last_seen_timestamp.insert(thread_id.clone(), conv.timestamp); + self.last_seen_timestamp + .insert(thread_id.clone(), conv.timestamp); } self.selected_thread = Some(thread_id.clone()); self.messages.clear(); @@ -373,7 +378,8 @@ impl Application for SmsWindow { Action::App(SmsMessage::RefreshThread) }), cosmic::task::future(async move { - kdeconnect_core::sms_read_state::save_last_seen(&device_id2, &last_seen).await; + kdeconnect_core::sms_read_state::save_last_seen(&device_id2, &last_seen) + .await; Action::None }), ]); @@ -436,16 +442,24 @@ impl Application for SmsWindow { // Update the conversation preview and timestamp so it sorts to // the top of the list immediately without waiting for a server refresh. - if let Some(conv) = self.conversations.iter_mut().find(|c| c.thread_id == thread_id) { + if let Some(conv) = self + .conversations + .iter_mut() + .find(|c| c.thread_id == thread_id) + { conv.last_message = text.clone(); conv.timestamp = now; } - self.conversations.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + self.conversations + .sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); // Scroll the conversation list to the top so the moved item is visible. let scroll_task = scrollable::scroll_to( views::CONVERSATIONS_SCROLLABLE_ID.clone(), - scrollable::AbsoluteOffset { x: Some(0.0), y: Some(0.0) }, + scrollable::AbsoluteOffset { + x: Some(0.0), + y: Some(0.0), + }, ); return Task::batch(vec![ @@ -458,7 +472,10 @@ impl Application for SmsWindow { } SmsMessage::RefreshThread => {} SmsMessage::ProtocolEventReceived(event) => { - debug!("ProtocolEventReceived: {:?}", std::mem::discriminant(&event)); + debug!( + "ProtocolEventReceived: {:?}", + std::mem::discriminant(&event) + ); self.handle_protocol_event(event); } SmsMessage::OpenNewChatDialog => { @@ -472,8 +489,8 @@ impl Application for SmsWindow { self.new_chat_phone_input = phone; } SmsMessage::SelectContactForNewChat(idx) => { - self.new_chat_phone_input = self.contacts[idx].0.clone(); - self.contact_idx = Some(idx); + self.new_chat_phone_input = self.contacts[idx].0.clone(); + self.contact_idx = Some(idx); } SmsMessage::CreateNewChat => { let phone = self.new_chat_phone_input.trim().to_string(); @@ -536,7 +553,9 @@ impl Application for SmsWindow { } fn dialog(&self) -> Option> { - self.pending_delete_thread.as_ref().map(|_| self.delete_confirm_dialog()) + self.pending_delete_thread + .as_ref() + .map(|_| self.delete_confirm_dialog()) } fn view(&self) -> Element<'_, Self::Message> { @@ -544,7 +563,7 @@ impl Application for SmsWindow { .width(Length::Fill) .height(Length::Fill) .align_x(cosmic::iced::Alignment::Center) - .into() + .into() } } @@ -581,7 +600,10 @@ impl SmsWindow { fn handle_protocol_event(&mut self, event: ProtocolEvent) { match event { ProtocolEvent::ConversationsReceived(conversations) => { - debug!("ConversationsReceived: {} conversations", conversations.len()); + debug!( + "ConversationsReceived: {} conversations", + conversations.len() + ); // Capture selected new_* phone BEFORE we mutate merged let pending_new_phone: Option = self @@ -599,10 +621,7 @@ impl SmsWindow { if let Some(pos) = merged.iter().position(|c| { c.thread_id.starts_with("new_") - && utils::phone_numbers_match( - &c.phone_number, - &incoming.phone_number, - ) + && utils::phone_numbers_match(&c.phone_number, &incoming.phone_number) }) { merged[pos] = incoming.clone(); } else if let Some(existing) = merged @@ -619,13 +638,14 @@ impl SmsWindow { if !c.thread_id.starts_with("new_") { return true; } - !conversations.iter().any(|r| { - utils::phone_numbers_match(&r.phone_number, &c.phone_number) - }) + !conversations + .iter() + .any(|r| utils::phone_numbers_match(&r.phone_number, &c.phone_number)) }); self.conversations = merged; - self.conversations.retain(|c| !self.hidden_conversations.contains(&c.thread_id)); + self.conversations + .retain(|c| !self.hidden_conversations.contains(&c.thread_id)); self.update_conversation_names(); // If we had a new_* selected, find its real thread by phone number now @@ -657,7 +677,8 @@ impl SmsWindow { let device_id = self.device_id.clone(); let last_seen = self.last_seen_timestamp.clone(); tokio::spawn(async move { - kdeconnect_core::sms_read_state::save_last_seen(&device_id, &last_seen).await; + kdeconnect_core::sms_read_state::save_last_seen(&device_id, &last_seen) + .await; }); let already_exists = self.messages.iter().any(|m| { diff --git a/cosmic-ext-connect-applet/src/plugins/sms/dbus.rs b/cosmic-ext-connect-applet/src/plugins/sms/dbus.rs index 88807bf4..eaa500a0 100755 --- a/cosmic-ext-connect-applet/src/plugins/sms/dbus.rs +++ b/cosmic-ext-connect-applet/src/plugins/sms/dbus.rs @@ -79,7 +79,10 @@ pub async fn fetch_conversations(device_id: &str) { } pub async fn request_conversation_messages(device_id: &str, thread_id: &str) { - debug!("request_conversation device={} thread={}", device_id, thread_id); + debug!( + "request_conversation device={} thread={}", + device_id, thread_id + ); let tid = thread_id.parse::().unwrap_or(0); use kdeconnect_varlink::iface::VarlinkClientInterface; let id = device_id.to_string(); @@ -99,10 +102,17 @@ pub async fn request_conversation_messages(device_id: &str, thread_id: &str) { } } -pub async fn send_sms(device_id: &str, phone_number: &str, message: &str, attachments: Vec) { +pub async fn send_sms( + device_id: &str, + phone_number: &str, + message: &str, + attachments: Vec, +) { debug!( "send_sms to={} device={} attachments={}", - phone_number, device_id, attachments.len() + phone_number, + device_id, + attachments.len() ); use kdeconnect_varlink::iface::VarlinkClientInterface; let id = device_id.to_string(); @@ -119,7 +129,10 @@ pub async fn send_sms(device_id: &str, phone_number: &str, message: &str, attach let Some(client) = get_client().await else { return; }; - match client.send_sms(device_id, phone_number, message, attachments).await { + match client + .send_sms(device_id, phone_number, message, attachments) + .await + { Ok(_) => debug!("send_sms OK"), Err(e) => error!("send_sms failed: {:?}", e), } @@ -129,7 +142,10 @@ pub async fn send_sms(device_id: &str, phone_number: &str, message: &str, attach /// — the result arrives later as a `SmsAttachmentReceived` D-Bus signal, /// same as every other async SMS event. pub async fn request_sms_attachment(device_id: &str, part_id: i64, unique_identifier: &str) { - debug!("request_sms_attachment device={} part_id={}", device_id, part_id); + debug!( + "request_sms_attachment device={} part_id={}", + device_id, part_id + ); use kdeconnect_varlink::iface::VarlinkClientInterface; let id = device_id.to_string(); let uid = unique_identifier.to_string(); @@ -143,7 +159,10 @@ pub async fn request_sms_attachment(device_id: &str, part_id: i64, unique_identi let Some(client) = get_client().await else { return; }; - match client.request_sms_attachment(device_id, part_id, unique_identifier).await { + match client + .request_sms_attachment(device_id, part_id, unique_identifier) + .await + { Ok(_) => debug!("request_sms_attachment sent"), Err(e) => error!("request_sms_attachment failed: {:?}", e), } @@ -200,7 +219,9 @@ pub async fn get_cached_contacts(device_id: &str) -> std::collections::HashMap std::collections::HashMap> { +pub async fn get_cached_contact_photos( + device_id: &str, +) -> std::collections::HashMap> { debug!("get_cached_contact_photos device={}", device_id); use kdeconnect_varlink::iface::VarlinkClientInterface; @@ -242,7 +263,11 @@ pub async fn get_cached_sms(device_id: &str) -> Option { use kdeconnect_varlink::iface::VarlinkClientInterface; let id = device_id.to_string(); if let Some(reply) = via_varlink(|c| async move { c.get_cached_sms(id).call().await }).await { - return if reply.json.is_empty() { None } else { Some(reply.json) }; + return if reply.json.is_empty() { + None + } else { + Some(reply.json) + }; } let Some(client) = get_client().await else { @@ -275,7 +300,11 @@ pub fn parse_sms_messages(messages_json: &str) -> (Vec, Vec bool { } /// Sort contacts alphabetically -pub fn sort_cached_contacts(contacts: std::collections::HashMap) -> Vec<(String, String)> { +pub fn sort_cached_contacts( + contacts: std::collections::HashMap, +) -> Vec<(String, String)> { let mut sorted_contacts: Vec<_> = contacts.clone().into_iter().collect(); sorted_contacts.sort_by(|a, b| a.1.cmp(&b.1)); sorted_contacts } - pub fn truncate_message(s: &str, max_len: usize) -> String { let mut char_count = 0; let mut byte_end = s.len(); diff --git a/cosmic-ext-connect-applet/src/plugins/sms/views.rs b/cosmic-ext-connect-applet/src/plugins/sms/views.rs index 4dda55a6..65613713 100755 --- a/cosmic-ext-connect-applet/src/plugins/sms/views.rs +++ b/cosmic-ext-connect-applet/src/plugins/sms/views.rs @@ -1,12 +1,12 @@ //! UI view implementations for the SMS window. +use cosmic::Element; use cosmic::iced::{Alignment, Length}; use cosmic::widget; -use cosmic::Element; use super::actions::SmsMessage; use super::app::SmsWindow; -use super::emoji::{is_emoji_char, EmojiCategory}; +use super::emoji::{EmojiCategory, is_emoji_char}; use super::models::Conversation; use super::utils::{format_timestamp, normalize_phone_number, phone_numbers_match}; diff --git a/cosmic-ext-connect-applet/src/portal.rs b/cosmic-ext-connect-applet/src/portal.rs index f17bf849..b3b03693 100755 --- a/cosmic-ext-connect-applet/src/portal.rs +++ b/cosmic-ext-connect-applet/src/portal.rs @@ -58,7 +58,10 @@ pub async fn pick_files( /// Opens a native "Save As" dialog and returns the chosen destination /// path, or `None` if cancelled/failed. -pub async fn save_file(title: impl Into, suggested_name: impl Into) -> Option { +pub async fn save_file( + title: impl Into, + suggested_name: impl Into, +) -> Option { let title_str = title.into(); let name_str = suggested_name.into(); diff --git a/cosmic-ext-connect-applet/src/settings.rs b/cosmic-ext-connect-applet/src/settings.rs index 505b92fa..b11f55b7 100755 --- a/cosmic-ext-connect-applet/src/settings.rs +++ b/cosmic-ext-connect-applet/src/settings.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate cosmic_ext_connect_applet; +use cosmic::cosmic_config::{ConfigGet, ConfigSet}; use cosmic::{ Action, Application, ApplicationExt, Element, Task, app::Core, @@ -10,7 +11,6 @@ use cosmic::{ use cosmic_ext_connect_applet::{backend, models::Device}; use futures::StreamExt as _; use std::collections::HashMap; -use cosmic::cosmic_config::{ConfigGet, ConfigSet}; /// A desktop command stored as JSON: {id, name, command} type LocalCommand = serde_json::Value; @@ -63,84 +63,84 @@ fn implemented_plugins() -> &'static [PluginInfo] { use std::sync::LazyLock; static PLUGINS: LazyLock> = LazyLock::new(|| { vec![ - PluginInfo { - id: "battery", - name: fl!("plugin-battery-name"), - description: fl!("plugin-battery-desc"), - icon: "battery-full-symbolic", - }, - PluginInfo { - id: "clipboard", - name: fl!("plugin-clipboard-name"), - description: fl!("plugin-clipboard-desc"), - icon: "edit-paste-symbolic", - }, - PluginInfo { - id: "connectivity_report", - name: fl!("plugin-connectivity-name"), - description: fl!("plugin-connectivity-desc"), - icon: "network-cellular-symbolic", - }, - PluginInfo { - id: "contacts", - name: fl!("plugin-contacts-name"), - description: fl!("plugin-contacts-desc"), - icon: "x-office-address-book-symbolic", - }, - PluginInfo { - id: "findmyphone", - name: fl!("plugin-findmyphone-name"), - description: fl!("plugin-findmyphone-desc"), - icon: "audio-speakers-symbolic", - }, - PluginInfo { - id: "mpris", - name: fl!("plugin-mpris-name"), - description: fl!("plugin-mpris-desc"), - icon: "media-playback-start-symbolic", - }, - PluginInfo { - id: "notification", - name: fl!("plugin-notifications-name"), - description: fl!("plugin-notifications-desc"), - icon: "preferences-system-notifications-symbolic", - }, - PluginInfo { - id: "ping", - name: fl!("plugin-ping-name"), - description: fl!("plugin-ping-desc"), - icon: "network-transmit-receive-symbolic", - }, - PluginInfo { - id: "runcommand", - name: fl!("plugin-runcommand-name"), - description: fl!("plugin-runcommand-desc"), - icon: "utilities-terminal-symbolic", - }, - PluginInfo { - id: "share", - name: fl!("plugin-share-name"), - description: fl!("plugin-share-desc"), - icon: "document-send-symbolic", - }, - PluginInfo { - id: "sms", - name: fl!("plugin-sms-name"), - description: fl!("plugin-sms-desc"), - icon: "mail-message-new-symbolic", - }, - PluginInfo { - id: "systemvolume", - name: fl!("plugin-systemvolume-name"), - description: fl!("plugin-systemvolume-desc"), - icon: "audio-volume-high-symbolic", - }, - PluginInfo { - id: "telephony", - name: fl!("plugin-telephony-name"), - description: fl!("plugin-telephony-desc"), - icon: "phone-symbolic", - }, + PluginInfo { + id: "battery", + name: fl!("plugin-battery-name"), + description: fl!("plugin-battery-desc"), + icon: "battery-full-symbolic", + }, + PluginInfo { + id: "clipboard", + name: fl!("plugin-clipboard-name"), + description: fl!("plugin-clipboard-desc"), + icon: "edit-paste-symbolic", + }, + PluginInfo { + id: "connectivity_report", + name: fl!("plugin-connectivity-name"), + description: fl!("plugin-connectivity-desc"), + icon: "network-cellular-symbolic", + }, + PluginInfo { + id: "contacts", + name: fl!("plugin-contacts-name"), + description: fl!("plugin-contacts-desc"), + icon: "x-office-address-book-symbolic", + }, + PluginInfo { + id: "findmyphone", + name: fl!("plugin-findmyphone-name"), + description: fl!("plugin-findmyphone-desc"), + icon: "audio-speakers-symbolic", + }, + PluginInfo { + id: "mpris", + name: fl!("plugin-mpris-name"), + description: fl!("plugin-mpris-desc"), + icon: "media-playback-start-symbolic", + }, + PluginInfo { + id: "notification", + name: fl!("plugin-notifications-name"), + description: fl!("plugin-notifications-desc"), + icon: "preferences-system-notifications-symbolic", + }, + PluginInfo { + id: "ping", + name: fl!("plugin-ping-name"), + description: fl!("plugin-ping-desc"), + icon: "network-transmit-receive-symbolic", + }, + PluginInfo { + id: "runcommand", + name: fl!("plugin-runcommand-name"), + description: fl!("plugin-runcommand-desc"), + icon: "utilities-terminal-symbolic", + }, + PluginInfo { + id: "share", + name: fl!("plugin-share-name"), + description: fl!("plugin-share-desc"), + icon: "document-send-symbolic", + }, + PluginInfo { + id: "sms", + name: fl!("plugin-sms-name"), + description: fl!("plugin-sms-desc"), + icon: "mail-message-new-symbolic", + }, + PluginInfo { + id: "systemvolume", + name: fl!("plugin-systemvolume-name"), + description: fl!("plugin-systemvolume-desc"), + icon: "audio-volume-high-symbolic", + }, + PluginInfo { + id: "telephony", + name: fl!("plugin-telephony-name"), + description: fl!("plugin-telephony-desc"), + icon: "phone-symbolic", + }, ] }); &PLUGINS @@ -228,10 +228,9 @@ impl SettingsApp { } fn refresh_devices_task() -> Task> { - Task::perform( - async { backend::fetch_devices().await }, - |devices| Action::App(Message::DevicesLoaded(devices)), - ) + Task::perform(async { backend::fetch_devices().await }, |devices| { + Action::App(Message::DevicesLoaded(devices)) + }) } } @@ -264,10 +263,8 @@ impl Application for SettingsApp { app.core.window.header_title = fl!("settings-title").into(); - let title_task = app.set_window_title( - fl!("settings-title"), - app.core.main_window_id().unwrap(), - ); + let title_task = + app.set_window_title(fl!("settings-title"), app.core.main_window_id().unwrap()); let load_task = Task::perform( async { @@ -279,10 +276,9 @@ impl Application for SettingsApp { |devices| Action::App(Message::DevicesLoaded(devices)), ); - let cmds_task = Task::perform( - async { load_run_commands() }, - |cmds| Action::App(Message::RunCommandsLoaded(cmds)), - ); + let cmds_task = Task::perform(async { load_run_commands() }, |cmds| { + Action::App(Message::RunCommandsLoaded(cmds)) + }); (app, Task::batch(vec![title_task, load_task, cmds_task])) } @@ -370,9 +366,7 @@ impl Application for SettingsApp { let pid = plugin_id; return Task::perform( async move { - if let Err(e) = - backend::set_plugin_enabled(did, pid, enabled).await - { + if let Err(e) = backend::set_plugin_enabled(did, pid, enabled).await { eprintln!("[settings] set_plugin_enabled failed: {:?}", e); } }, @@ -497,10 +491,7 @@ impl Application for SettingsApp { // --------------------------------------------------------------------------- impl SettingsApp { - fn view_tab_bar<'a>( - &'a self, - spacing: &cosmic::cosmic_theme::Spacing, - ) -> Element<'a, Message> { + fn view_tab_bar<'a>(&'a self, spacing: &cosmic::cosmic_theme::Spacing) -> Element<'a, Message> { let paired_btn = if self.active_tab == Tab::PairedDevices { widget::button::standard(fl!("settings-tab-paired")) .on_press(Message::SelectTab(Tab::PairedDevices)) @@ -549,11 +540,8 @@ impl SettingsApp { if paired.is_empty() { col = col.push( - widget::container( - widget::text(fl!("paired-devices-none")) - .size(12), - ) - .padding(spacing.space_s), + widget::container(widget::text(fl!("paired-devices-none")).size(12)) + .padding(spacing.space_s), ); } else { for device in &paired { @@ -587,12 +575,21 @@ impl SettingsApp { if is_selected { col = col.push( - widget::container(widget::button::custom(item).width(Length::Fill).on_press(Message::SelectDevice(device_id)).class(cosmic::theme::Button::Suggested)) - .class(cosmic::theme::Container::Primary) - .width(Length::Fill), + widget::container( + widget::button::custom(item) + .width(Length::Fill) + .on_press(Message::SelectDevice(device_id)) + .class(cosmic::theme::Button::Suggested), + ) + .class(cosmic::theme::Container::Primary) + .width(Length::Fill), ); } else { - col = col.push(widget::button::custom(item).class(cosmic::theme::Button::Standard).on_press(Message::SelectDevice(device_id))); + col = col.push( + widget::button::custom(item) + .class(cosmic::theme::Button::Standard) + .on_press(Message::SelectDevice(device_id)), + ); } } } @@ -641,10 +638,8 @@ impl SettingsApp { if self.selected_device.is_none() { col = col.push( - widget::container( - widget::text(fl!("paired-plugins-hint")).size(14), - ) - .padding(spacing.space_l), + widget::container(widget::text(fl!("paired-plugins-hint")).size(14)) + .padding(spacing.space_l), ); return widget::scrollable(col).height(Length::Fill).into(); } @@ -673,7 +668,10 @@ impl SettingsApp { .push(widget::text(plugin.description.as_str()).size(12)) .width(Length::Fill), ) - .push(widget::toggler(enabled).on_toggle(move |f| Message::TogglePlugin(plugin_id.clone(), f))); + .push( + widget::toggler(enabled) + .on_toggle(move |f| Message::TogglePlugin(plugin_id.clone(), f)), + ); col = col.push( widget::container(row) @@ -716,12 +714,12 @@ impl SettingsApp { .font(cosmic::font::bold()) .width(Length::Fill), ) - .push(widget::button::standard(fl!("settings-scan-again")).on_press(Message::Refresh)), + .push( + widget::button::standard(fl!("settings-scan-again")).on_press(Message::Refresh), + ), ); col = col.push(widget::divider::horizontal::default()); - col = col.push( - widget::text(fl!("available-devices-hint")).size(13), - ); + col = col.push(widget::text(fl!("available-devices-hint")).size(13)); if available.is_empty() { col = col.push( @@ -734,11 +732,11 @@ impl SettingsApp { .size(16) .font(cosmic::font::bold()), ) + .push(widget::text(fl!("available-devices-none-hint")).size(13)) .push( - widget::text(fl!("available-devices-none-hint")) - .size(13), + widget::button::standard(fl!("settings-scan-again")) + .on_press(Message::Refresh), ) - .push(widget::button::standard(fl!("settings-scan-again")).on_press(Message::Refresh)) .align_x(Alignment::Center), ) .padding([spacing.space_xl, spacing.space_m]) @@ -836,13 +834,16 @@ impl SettingsApp { .width(Length::Fill), ); col = col.push( - widget::text_input(fl!("run-commands-command-placeholder"), &self.new_cmd_command) - .on_input(Message::NewRunCommandCommand) - .width(Length::Fill), + widget::text_input( + fl!("run-commands-command-placeholder"), + &self.new_cmd_command, + ) + .on_input(Message::NewRunCommandCommand) + .width(Length::Fill), ); col = col.push( widget::button::suggested(fl!("run-commands-add-button")) - .on_press(Message::AddRunCommand) + .on_press(Message::AddRunCommand), ); widget::container(col) @@ -854,7 +855,6 @@ impl SettingsApp { } fn main() -> cosmic::iced::Result { - let settings = cosmic::app::Settings::default() - .size(cosmic::iced::Size::new(740.0, 540.0)); + let settings = cosmic::app::Settings::default().size(cosmic::iced::Size::new(740.0, 540.0)); cosmic::app::run::(settings, ()) } diff --git a/cosmic-ext-connect-applet/src/theme.rs b/cosmic-ext-connect-applet/src/theme.rs index 8e7ddc06..83c5be94 100644 --- a/cosmic-ext-connect-applet/src/theme.rs +++ b/cosmic-ext-connect-applet/src/theme.rs @@ -183,9 +183,7 @@ pub fn accent_icon(name: &str, accent: cosmic::iced::Color) -> cosmic::widget::i let handle = cosmic::widget::icon::from_name(name) .path() .and_then(|path| std::fs::read_to_string(path).ok()) - .map(|svg| { - cosmic::widget::icon::from_svg_bytes(svg.replace("#232323", &hex).into_bytes()) - }) + .map(|svg| cosmic::widget::icon::from_svg_bytes(svg.replace("#232323", &hex).into_bytes())) .unwrap_or_else(|| cosmic::widget::icon::from_name(name).handle()); cache.lock().unwrap().insert(key, handle.clone()); diff --git a/cosmic-ext-connect-applet/src/ui/popup.rs b/cosmic-ext-connect-applet/src/ui/popup.rs index ec4ef042..a56c8754 100755 --- a/cosmic-ext-connect-applet/src/ui/popup.rs +++ b/cosmic-ext-connect-applet/src/ui/popup.rs @@ -1,12 +1,14 @@ - -use crate::messages::Message; +use crate::messages::{self, Message}; use crate::models::{Device, NowPlaying}; use cosmic::app::Core; +use cosmic::iced::core::text::Wrapping; use cosmic::iced::{Alignment, Length}; -use cosmic::widget::Row; -use cosmic::{widget, Element}; +use cosmic::widget::{Row, icon, settings, space::horizontal, text}; +use cosmic::{Element, theme, widget}; use std::collections::HashMap; +const APPLET_VERSION: &str = env!("CARGO_PKG_VERSION"); + /// Build the popup view using the real application Core so popup_container /// has proper applet context, theme, and sizing. pub fn create_popup_view<'a>( @@ -19,24 +21,26 @@ pub fn create_popup_view<'a>( now_playing: &'a HashMap, ) -> Element<'a, Message> { let spacing = cosmic::theme::active().cosmic().spacing; - let mut content = widget::Column::new() - .spacing(spacing.space_s) - .padding(spacing.space_s); + + let mut content = widget::Column::new().padding(spacing.space_s); + + let about_icon = widget::button::icon(widget::icon::from_name("help-about-symbolic")) + .on_press(Message::SwitchPage(messages::Page::About)); + let settings_icon = widget::button::icon(widget::icon::from_name("application-menu-symbolic")) + .on_press(Message::OpenSettings); // Header content = content.push( Row::new() - .push( - widget::text(fl!("applet-title")) - .size(18) - .width(Length::Fill), - ) - .push(widget::button::standard(fl!("applet-settings")).on_press(Message::OpenSettings)) - .spacing(spacing.space_xs) + .push(about_icon) + .push(horizontal()) + .push(settings_icon) .align_y(Alignment::Center), ); - content = content.push(widget::divider::horizontal::default()); + content = content + .push(widget::divider::horizontal::default()) + .spacing(spacing.space_xxs); // Dismissible error banner — surfaces failures (e.g. browse-device // preflight checks) that used to be silently dropped. @@ -241,7 +245,8 @@ fn create_device_card<'a>( if is_expanded && is_online { let mut menu_items = widget::Column::new().spacing(spacing.space_xxs); - let mut quick_actions_list = widget::list_column().style(cosmic::theme::Container::Transparent); + let mut quick_actions_list = + widget::list_column().style(cosmic::theme::Container::Transparent); quick_actions_list = quick_actions_list.add(widget::text::caption_heading(fl!("quick-actions-header"))); @@ -370,10 +375,7 @@ fn create_device_card<'a>( menu_items = menu_items.push(quick_actions_list); - col = col.push( - widget::container(menu_items) - .padding([spacing.space_xs, spacing.space_m]) - ); + col = col.push(widget::container(menu_items).padding([spacing.space_xs, spacing.space_m])); } else if is_expanded && !is_online { col = col.push( widget::container(widget::text(fl!("devices-not-reachable")).size(12)) @@ -483,3 +485,109 @@ fn create_media_card<'a>( .width(Length::Fill) .into() } + +/// Builds About page view +pub fn about_view<'a>(core: &'a Core) -> Element<'static, Message> { + let spacing = cosmic::theme::active().cosmic().spacing; + + let mut content = widget::Column::new().padding(spacing.space_xs); + + let back_button = widget::button::custom(settings::item_row(vec![ + icon::from_name("go-previous-symbolic") + .size(16) + .icon() + .into(), + text::body("Back") + .width(Length::Fill) + .wrapping(Wrapping::Word) + .into(), + ])) + .on_press(messages::Message::SwitchPage(messages::Page::Dashboard)) + .class(theme::Button::Link); + + // Header + content = content.push(back_button); + + // Center area + // Icon + content = content.push( + widget::container( + widget::icon::from_name("io.github.hepp3n.kdeconnect") + .prefer_svg(true) + .size(64), + ) + .center_x(Length::Fill), + ); + // Applet name + content = content.push( + widget::container(widget::text::title3(fl!("applet-title")).align_x(Alignment::Center)) + .center_x(Length::Fill), + ); + // Applet author + content = content.push( + widget::container(widget::text::caption_heading("heppen").align_x(Alignment::Center)) + .center_x(Length::Fill), + ); + + // Applet version + content = content.push( + widget::container(widget::button::standard(APPLET_VERSION)) + .padding([spacing.space_xxs, 0, 0, 0]) + .center_x(Length::Fill), + ); + + // Links section + content = content + .push(widget::text::body("Links")) + .spacing(spacing.space_xxs); + + let links = widget::settings::section() + .add( + widget::list::button( + widget::row::with_capacity(3) + .align_y(Alignment::Center) + .push(widget::text::body("Repository").width(Length::Fill)) + .push(widget::icon::from_name("link-symbolic").icon()), + ) + .on_press(messages::Message::OpenRepository), + ) + .add( + widget::list::button( + widget::row::with_capacity(3) + .align_y(Alignment::Center) + .push(widget::text::body("Support").width(Length::Fill)) + .push(widget::icon::from_name("link-symbolic").icon()), + ) + .on_press(messages::Message::OpenSupport), + ); + + content = content.push(links); + + // License + content = content + .push(widget::text::body("License")) + .spacing(spacing.space_xxs); + + let license = widget::settings::section().add( + widget::list::button( + widget::row::with_capacity(3) + .align_y(Alignment::Center) + .push(widget::text::body("GPL-3.0 only").width(Length::Fill)) + .push(widget::icon::from_name("link-symbolic").icon()), + ) + .on_press(messages::Message::OpenLicense), + ); + + content = content.push(license); + + // Description + content = content.push(widget::text::body(fl!("applet-description"))); + + let popup_content = widget::container(widget::scrollable(content)) + .width(Length::Fixed(400.0)) + .max_height(700.0) + .padding(spacing.space_xs); + + // Use the real Core so the popup has proper applet context and theme + core.applet.popup_container(popup_content).into() +} diff --git a/kdeconnect-core/src/contacts.rs b/kdeconnect-core/src/contacts.rs index 9842396a..a6acd4c1 100644 --- a/kdeconnect-core/src/contacts.rs +++ b/kdeconnect-core/src/contacts.rs @@ -10,4 +10,4 @@ pub fn decode_photo(base64_encoded: &str) -> Option> { use base64::{Engine as _, engine::general_purpose}; general_purpose::STANDARD.decode(base64_encoded).ok() -} \ No newline at end of file +} diff --git a/kdeconnect-core/src/hidden_conversations.rs b/kdeconnect-core/src/hidden_conversations.rs index e5362771..2dc58dd9 100644 --- a/kdeconnect-core/src/hidden_conversations.rs +++ b/kdeconnect-core/src/hidden_conversations.rs @@ -47,7 +47,11 @@ pub async fn save_hidden(device_id: &str, hidden: &HashSet) { match serde_json::to_string(hidden) { Ok(json) => { if let Err(e) = tokio::fs::write(&path, json).await { - tracing::warn!("[hidden_conversations] failed to save for {}: {}", device_id, e); + tracing::warn!( + "[hidden_conversations] failed to save for {}: {}", + device_id, + e + ); } } Err(e) => tracing::warn!("[hidden_conversations] serialize failed: {}", e), diff --git a/kdeconnect-core/src/lib.rs b/kdeconnect-core/src/lib.rs index f283d348..36580d74 100644 --- a/kdeconnect-core/src/lib.rs +++ b/kdeconnect-core/src/lib.rs @@ -21,17 +21,17 @@ use crate::{ pub mod config; pub mod contacts; -pub mod plugin_config; -pub mod hidden_conversations; -pub mod sms_read_state; pub(crate) mod crypto; pub mod device; pub mod event; pub mod filetransfer; +pub mod hidden_conversations; pub(crate) mod pairing; +pub mod plugin_config; pub(crate) mod plugin_interface; pub mod plugins; pub(crate) mod protocol; +pub mod sms_read_state; pub(crate) mod transport; // Re-export commonly used protocol types for external crates @@ -121,10 +121,7 @@ impl KdeConnectCore { pub async fn run_event_loop(&mut self) { info!("Starting KdeConnect event loop"); - plugins::mpris::monitor_mpris( - (*self.device_manager).clone(), - self.event_tx.clone(), - ); + plugins::mpris::monitor_mpris((*self.device_manager).clone(), self.event_tx.clone()); loop { select! { @@ -204,12 +201,11 @@ impl KdeConnectCore { if let Some(sender) = guard.get(&device_id) { let mpris_pkt = ProtocolPacket::new( PacketType::MprisRequest, - serde_json::to_value( - crate::plugins::mpris::MprisRequest { - request_player_list: Some(true), - ..Default::default() - } - ).unwrap(), + serde_json::to_value(crate::plugins::mpris::MprisRequest { + request_player_list: Some(true), + ..Default::default() + }) + .unwrap(), ); let _ = sender.send(mpris_pkt); } @@ -304,8 +300,12 @@ impl KdeConnectCore { // Send our local command list so the Android app shows // the Run Command option (requires canAddCommand: true). plugins::run_command::send_command_list(&id, self.event_tx.clone()).await; - - if self.plugin_registry.is_plugin_enabled(&id.0, "systemvolume").await { + + if self + .plugin_registry + .is_plugin_enabled(&id.0, "systemvolume") + .await + { plugins::systemvolume::on_device_connect(id.clone(), self.event_tx.clone()); } } @@ -329,9 +329,15 @@ impl KdeConnectCore { // user can still initiate pairing normally from the settings app. self.pending_pair.lock().await.remove(&id); if device.pair_state == crate::device::PairState::Paired { - info!("[core] Phone unpairing from us — cleaning up {}", id); + info!( + "[core] Phone unpairing from us — cleaning up {}", + id + ); self.device_manager - .update_pair_state(&id, crate::device::PairState::NotPaired) + .update_pair_state( + &id, + crate::device::PairState::NotPaired, + ) .await; cleanup_device_data(&id.0).await; let conn_event = ConnectionEvent::PairStateChanged(( @@ -340,7 +346,10 @@ impl KdeConnectCore { )); self.broadcast_conn_event(conn_event); } else { - info!("[core] pair:false from {} — device not paired, ignoring", id); + info!( + "[core] pair:false from {} — device not paired, ignoring", + id + ); } } else { let device_name = device.name.clone(); @@ -601,7 +610,10 @@ impl KdeConnectCore { // The phone processes capabilities only during connection setup. if guard.remove(&device_id).is_some() { self.conn_id_map.lock().await.remove(&device_id); - info!("[plugin] dropped connection to {} — phone will reconnect with updated capabilities", device_id); + info!( + "[plugin] dropped connection to {} — phone will reconnect with updated capabilities", + device_id + ); } } }; @@ -645,7 +657,11 @@ async fn cleanup_device_data(device_id: &str) { .join(format!("{}_plugins.json", device_id)); if let Err(e) = tokio::fs::remove_file(&plugin_file).await { if e.kind() != std::io::ErrorKind::NotFound { - tracing::warn!("[cleanup] failed to remove plugin config for {}: {}", device_id, e); + tracing::warn!( + "[cleanup] failed to remove plugin config for {}: {}", + device_id, + e + ); } } } @@ -655,7 +671,11 @@ async fn cleanup_device_data(device_id: &str) { let cache_dir = data_dir.join("kdeconnect").join(device_id); if let Err(e) = tokio::fs::remove_dir_all(&cache_dir).await { if e.kind() != std::io::ErrorKind::NotFound { - tracing::warn!("[cleanup] failed to remove cache dir for {}: {}", device_id, e); + tracing::warn!( + "[cleanup] failed to remove cache dir for {}: {}", + device_id, + e + ); } } } diff --git a/kdeconnect-core/src/pairing.rs b/kdeconnect-core/src/pairing.rs index 45035ff8..8518dc5a 100644 --- a/kdeconnect-core/src/pairing.rs +++ b/kdeconnect-core/src/pairing.rs @@ -74,7 +74,10 @@ impl PairingManager { .update_pair_state(&id, PairState::Requested) .await; - info!("Pair request received from {} — awaiting user decision", name); + info!( + "Pair request received from {} — awaiting user decision", + name + ); Ok(true) } diff --git a/kdeconnect-core/src/plugin_interface.rs b/kdeconnect-core/src/plugin_interface.rs index 158f0887..5463a81f 100644 --- a/kdeconnect-core/src/plugin_interface.rs +++ b/kdeconnect-core/src/plugin_interface.rs @@ -12,7 +12,7 @@ use crate::{ GLOBAL_CONFIG, device::Device, event::{ConnectionEvent, CoreEvent}, - filetransfer::{send_progress, TransferAdapter}, + filetransfer::{TransferAdapter, send_progress}, plugins::{ self, battery::Battery, @@ -112,10 +112,7 @@ impl PluginRegistry { ) { // Gate on plugin enabled state before doing any work. if let Some(plugin_id) = packet_plugin_id(&packet.packet_type) { - if !self - .is_plugin_enabled(&device.device_id.0, plugin_id) - .await - { + if !self.is_plugin_enabled(&device.device_id.0, plugin_id).await { debug!( "[plugin_registry] packet {:?} skipped — plugin '{}' disabled for {}", packet.packet_type, plugin_id, device.device_id @@ -245,7 +242,9 @@ impl PluginRegistry { debug!("Parsed {} phone->name contact entries", contacts.len()); debug!( "[contacts] {} vcards: {} photos decoded inline, {} referenced remote URLs (not fetched)", - vcard_count, photos.len(), remote_photo_count + vcard_count, + photos.len(), + remote_photo_count ); if !contacts.is_empty() { let _ = connection_tx.send(ConnectionEvent::ContactsReceived(contacts)); @@ -385,9 +384,9 @@ impl PluginRegistry { tokio::spawn(async move { match info.browse(&device_id.0, &device_name).await { Ok(_) => { - let _ = connection_tx.send( - ConnectionEvent::SftpMountStateChanged((device_id, true)), - ); + let _ = connection_tx.send(ConnectionEvent::SftpMountStateChanged( + (device_id, true), + )); } Err(e) => { warn!("[sftp] browse failed: {}", e); @@ -553,7 +552,8 @@ fn decode_quoted_printable(input: &str) -> String { let mut out: Vec = Vec::with_capacity(bytes.len()); let mut i = 0; while i < bytes.len() { - if bytes[i] == b'=' && i + 2 < bytes.len() + if bytes[i] == b'=' + && i + 2 < bytes.len() && bytes[i + 1].is_ascii_hexdigit() && bytes[i + 2].is_ascii_hexdigit() { @@ -626,9 +626,10 @@ fn extract_vcard_photo(prop_upper: &str, value: &str) -> VcardPhoto { } } - let has_base64_param = prop_upper.split(';').skip(1).any(|param| { - matches!(param, "BASE64" | "B" | "ENCODING=BASE64" | "ENCODING=B") - }); + let has_base64_param = prop_upper + .split(';') + .skip(1) + .any(|param| matches!(param, "BASE64" | "B" | "ENCODING=BASE64" | "ENCODING=B")); if has_base64_param { let stripped: String = value.chars().filter(|c| !c.is_whitespace()).collect(); diff --git a/kdeconnect-core/src/plugins/mpris.rs b/kdeconnect-core/src/plugins/mpris.rs index 2f0a9089..212239b0 100644 --- a/kdeconnect-core/src/plugins/mpris.rs +++ b/kdeconnect-core/src/plugins/mpris.rs @@ -220,7 +220,10 @@ pub fn get_all_mpris_player_names() -> Vec { Err(_) => return vec![], }; match finder.find_all() { - Ok(players) => players.into_iter().map(|p| p.identity().to_string()).collect(), + Ok(players) => players + .into_iter() + .map(|p| p.identity().to_string()) + .collect(), Err(_) => vec![], } } @@ -419,7 +422,7 @@ pub fn monitor_mpris( let ctx_sup = core_tx.clone(); tokio::task::spawn_blocking(move || { - let (call_tx, call_rx) = std::sync::mpsc::sync_channel::(1); + let (call_tx, call_rx) = std::sync::mpsc::sync_channel::(1); TELEPHONY_CALL_TX.set(call_tx).ok(); // Holds Player objects paused for an active call — keeping them alive @@ -614,7 +617,10 @@ impl PhoneMprisPlayer { self.send_request(request).await; } - async fn play_pause(&self, #[zbus(signal_emitter)] emitter: zbus::object_server::SignalEmitter<'_>) { + async fn play_pause( + &self, + #[zbus(signal_emitter)] emitter: zbus::object_server::SignalEmitter<'_>, + ) { let mut state = self.player_state.write().await; let is_playing = state.is_playing.unwrap_or(false); state.is_playing = Some(!is_playing); diff --git a/kdeconnect-core/src/plugins/notification.rs b/kdeconnect-core/src/plugins/notification.rs index 189d21dd..2ddaba28 100644 --- a/kdeconnect-core/src/plugins/notification.rs +++ b/kdeconnect-core/src/plugins/notification.rs @@ -48,7 +48,10 @@ impl Notification { notify.action(action, action); } - notify.hint(notify_rust::Hint::Resident(true)).show().unwrap(); + notify + .hint(notify_rust::Hint::Resident(true)) + .show() + .unwrap(); }) .await; } diff --git a/kdeconnect-core/src/plugins/run_command.rs b/kdeconnect-core/src/plugins/run_command.rs index 46cfcc4f..5db7913b 100644 --- a/kdeconnect-core/src/plugins/run_command.rs +++ b/kdeconnect-core/src/plugins/run_command.rs @@ -85,7 +85,10 @@ impl RunCommand { match serde_json::from_str(&self.command_list) { Ok(map) => map, Err(e) => { - warn!("[runcommand] failed to parse commandList from {}: {}", device.device_id, e); + warn!( + "[runcommand] failed to parse commandList from {}: {}", + device.device_id, e + ); return; } }; @@ -136,10 +139,7 @@ impl RunCommandRequest { // Phone is asking us to execute a local command by its UUID key. let commands = load_local_commands(); if let Some(cmd) = commands.iter().find(|c| c.id == *key) { - info!( - "[runcommand] executing '{}': {}", - cmd.name, cmd.command - ); + info!("[runcommand] executing '{}': {}", cmd.name, cmd.command); let result = if std::env::var("FLATPAK_ID").is_ok() { std::process::Command::new("flatpak-spawn") .arg("--host") @@ -153,8 +153,7 @@ impl RunCommandRequest { .arg(&cmd.command) .spawn() }; - if let Err(e) = result - { + if let Err(e) = result { warn!("[runcommand] failed to spawn '{}': {}", cmd.name, e); } } else { @@ -194,8 +193,8 @@ pub async fn send_command_list( serde_json::json!({ "name": cmd.name, "command": cmd.command }), ); } - let command_list_str = serde_json::to_string(&serde_json::Value::Object(map)) - .unwrap_or_else(|_| "{}".to_string()); + let command_list_str = + serde_json::to_string(&serde_json::Value::Object(map)).unwrap_or_else(|_| "{}".to_string()); info!( "[runcommand] sending {} command(s) to {}", diff --git a/kdeconnect-core/src/plugins/sms.rs b/kdeconnect-core/src/plugins/sms.rs index ef5acc1b..50af8042 100644 --- a/kdeconnect-core/src/plugins/sms.rs +++ b/kdeconnect-core/src/plugins/sms.rs @@ -228,10 +228,7 @@ pub async fn build_send_packet( /// in-memory, while a thread is open) and the panel applet (which only /// needs this one summary bool for its unread badge) so the grouping /// logic isn't duplicated between the two processes. -pub fn has_unread( - messages_json: &str, - last_seen: &std::collections::HashMap, -) -> bool { +pub fn has_unread(messages_json: &str, last_seen: &std::collections::HashMap) -> bool { let Ok(data) = serde_json::from_str::(messages_json) else { return false; }; @@ -239,7 +236,9 @@ pub fn has_unread( let mut latest_by_thread: std::collections::HashMap = std::collections::HashMap::new(); for msg in &data.messages { - let entry = latest_by_thread.entry(msg.thread_id).or_insert((msg.date, msg.read == 0)); + let entry = latest_by_thread + .entry(msg.thread_id) + .or_insert((msg.date, msg.read == 0)); if msg.date > entry.0 { *entry = (msg.date, msg.read == 0); } else if msg.date == entry.0 && msg.read == 0 { @@ -247,10 +246,12 @@ pub fn has_unread( } } - latest_by_thread.into_iter().any(|(thread_id, (date, phone_unread))| { - match last_seen.get(&thread_id.to_string()) { - Some(&seen_at) => date > seen_at, - None => phone_unread, - } - }) + latest_by_thread + .into_iter() + .any( + |(thread_id, (date, phone_unread))| match last_seen.get(&thread_id.to_string()) { + Some(&seen_at) => date > seen_at, + None => phone_unread, + }, + ) } diff --git a/kdeconnect-core/src/plugins/systemvolume.rs b/kdeconnect-core/src/plugins/systemvolume.rs index bd1947d8..4d01144e 100644 --- a/kdeconnect-core/src/plugins/systemvolume.rs +++ b/kdeconnect-core/src/plugins/systemvolume.rs @@ -88,8 +88,10 @@ static PA_CONNECTIONS: std::sync::LazyLock Option) -> Option for PacketType { if let Some(entry) = PACKET_TYPES.iter().find(|entry| entry.0 == value.as_str()) { return entry.1.clone(); } - if let Some(entry) = PACKET_TYPE_ALIASES.iter().find(|entry| entry.0 == value.as_str()) { + if let Some(entry) = PACKET_TYPE_ALIASES + .iter() + .find(|entry| entry.0 == value.as_str()) + { return entry.1.clone(); } tracing::debug!("Unknown packet type received: {}", value); diff --git a/kdeconnect-core/src/transport.rs b/kdeconnect-core/src/transport.rs index 380aa8ec..c53ef606 100644 --- a/kdeconnect-core/src/transport.rs +++ b/kdeconnect-core/src/transport.rs @@ -67,9 +67,12 @@ pub enum TransportEvent { /// Build a raw `kdeconnect.identity` packet ready to write to a socket. /// Used for both pre-TLS and post-TLS identity exchange on both transports. fn identity_raw(identity: &Identity) -> Vec { - ProtocolPacket::new(PacketType::Identity, serde_json::to_value(identity).unwrap()) - .as_raw() - .expect("Failed to serialize identity packet") + ProtocolPacket::new( + PacketType::Identity, + serde_json::to_value(identity).unwrap(), + ) + .as_raw() + .expect("Failed to serialize identity packet") } /// Completes the identity/TLS handshake once a TCP stream to the peer exists @@ -209,7 +212,9 @@ impl TcpTransport { } Ok(_) => { raw.push(byte[0]); - if byte[0] == b'\n' { break; } + if byte[0] == b'\n' { + break; + } if raw.len() > 65536 { warn!(peer = ?peer, "[tcp] identity line too long"); break; @@ -309,11 +314,17 @@ impl UdpTransport { tracing::error!( "UDP port {} still in use after {} attempts — \ another instance may be running, exiting: {}", - config.listen_addr.port(), attempts, e + config.listen_addr.port(), + attempts, + e ); std::process::exit(1); } - tracing::warn!("UDP bind failed (attempt {}), retrying in 1s: {}", attempts, e); + tracing::warn!( + "UDP bind failed (attempt {}), retrying in 1s: {}", + attempts, + e + ); tokio::time::sleep(std::time::Duration::from_secs(1)).await; } } @@ -415,8 +426,16 @@ impl UdpTransport { return; } }; - complete_handshake(stream, identity, server_config, peer, id, name, event_tx) - .await; + complete_handshake( + stream, + identity, + server_config, + peer, + id, + name, + event_tx, + ) + .await; }); } Err(e) => { @@ -521,24 +540,73 @@ async fn filtered_identity_for_device(device_id: &str) -> Identity { // Map plugin IDs to the capability strings they own. // (incoming_caps, outgoing_caps) let cap_map: &[(&str, &[&str], &[&str])] = &[ - ("battery", &["kdeconnect.battery"], &["kdeconnect.battery.request"]), - ("clipboard", &["kdeconnect.clipboard", "kdeconnect.clipboard.connect"], &["kdeconnect.clipboard"]), - ("connectivity_report", &["kdeconnect.connectivity_report"], &[]), - ("contacts", &["kdeconnect.contacts.response_uids_timestamps", - "kdeconnect.contacts.response_vcards"], &["kdeconnect.contacts.request_all_uids_timestamps", - "kdeconnect.contacts.request_vcards_by_uid"]), - ("findmyphone", &[], &["kdeconnect.findmyphone.request"]), - ("mpris", &["kdeconnect.mpris", "kdeconnect.mpris.request"], &["kdeconnect.mpris", "kdeconnect.mpris.request"]), - ("notification", &["kdeconnect.notification"], &["kdeconnect.notification.request"]), - ("ping", &["kdeconnect.ping"], &["kdeconnect.ping"]), - ("runcommand", &["kdeconnect.runcommand.request"], &["kdeconnect.runcommand"]), - ("sftp", &["kdeconnect.sftp"], &["kdeconnect.sftp.request"]), - ("share", &["kdeconnect.share.request"], &["kdeconnect.share.request", "kdeconnect.share.request.update"]), - ("sms", &["kdeconnect.sms.messages", "kdeconnect.sms.attachment_file"], &["kdeconnect.sms.request", - "kdeconnect.sms.request_conversations", - "kdeconnect.sms.request_conversation", - "kdeconnect.sms.request_attachment"]), - ("telephony", &["kdeconnect.telephony"], &["kdeconnect.telephony.request_mute"]), + ( + "battery", + &["kdeconnect.battery"], + &["kdeconnect.battery.request"], + ), + ( + "clipboard", + &["kdeconnect.clipboard", "kdeconnect.clipboard.connect"], + &["kdeconnect.clipboard"], + ), + ( + "connectivity_report", + &["kdeconnect.connectivity_report"], + &[], + ), + ( + "contacts", + &[ + "kdeconnect.contacts.response_uids_timestamps", + "kdeconnect.contacts.response_vcards", + ], + &[ + "kdeconnect.contacts.request_all_uids_timestamps", + "kdeconnect.contacts.request_vcards_by_uid", + ], + ), + ("findmyphone", &[], &["kdeconnect.findmyphone.request"]), + ( + "mpris", + &["kdeconnect.mpris", "kdeconnect.mpris.request"], + &["kdeconnect.mpris", "kdeconnect.mpris.request"], + ), + ( + "notification", + &["kdeconnect.notification"], + &["kdeconnect.notification.request"], + ), + ("ping", &["kdeconnect.ping"], &["kdeconnect.ping"]), + ( + "runcommand", + &["kdeconnect.runcommand.request"], + &["kdeconnect.runcommand"], + ), + ("sftp", &["kdeconnect.sftp"], &["kdeconnect.sftp.request"]), + ( + "share", + &["kdeconnect.share.request"], + &[ + "kdeconnect.share.request", + "kdeconnect.share.request.update", + ], + ), + ( + "sms", + &["kdeconnect.sms.messages", "kdeconnect.sms.attachment_file"], + &[ + "kdeconnect.sms.request", + "kdeconnect.sms.request_conversations", + "kdeconnect.sms.request_conversation", + "kdeconnect.sms.request_attachment", + ], + ), + ( + "telephony", + &["kdeconnect.telephony"], + &["kdeconnect.telephony.request_mute"], + ), ]; let mut remove_inc: std::collections::HashSet<&str> = std::collections::HashSet::new(); @@ -556,11 +624,15 @@ async fn filtered_identity_for_device(device_id: &str) -> Identity { device_type: base.device_type, protocol_version: base.protocol_version, tcp_port: base.tcp_port, - incoming_capabilities: base.incoming_capabilities.iter() + incoming_capabilities: base + .incoming_capabilities + .iter() .filter(|c| !remove_inc.contains(c.as_str())) .cloned() .collect(), - outgoing_capabilities: base.outgoing_capabilities.iter() + outgoing_capabilities: base + .outgoing_capabilities + .iter() .filter(|c| !remove_out.contains(c.as_str())) .cloned() .collect(), diff --git a/kdeconnect-dbus-client/src/lib.rs b/kdeconnect-dbus-client/src/lib.rs index 5a53d3f2..d685ae38 100755 --- a/kdeconnect-dbus-client/src/lib.rs +++ b/kdeconnect-dbus-client/src/lib.rs @@ -363,7 +363,10 @@ impl KdeConnectClient { /// Get cached contact photos as a raw JSON string (phone → base64) pub async fn get_cached_contact_photos(&self, device_id: &str) -> Result { - Ok(self.contacts_proxy.get_cached_contact_photos(device_id).await?) + Ok(self + .contacts_proxy + .get_cached_contact_photos(device_id) + .await?) } /// Create a stream of service events @@ -426,7 +429,9 @@ impl KdeConnectClient { .await? .filter_map(|s| async move { match s.args() { - Ok(args) => Some(ServiceEvent::SmsMessagesReceived(args.messages_json.clone())), + Ok(args) => Some(ServiceEvent::SmsMessagesReceived( + args.messages_json.clone(), + )), Err(e) => { error!("Failed to parse SmsMessagesReceived signal: {:?}", e); None @@ -535,22 +540,22 @@ impl KdeConnectClient { } }); - let browse_failed = self - .daemon_proxy - .receive_browse_failed() - .await? - .filter_map(|s| async move { - match s.args() { - Ok(args) => Some(ServiceEvent::BrowseFailed( - args.device_id.clone(), - args.message.clone(), - )), - Err(e) => { - error!("Failed to parse BrowseFailed signal: {:?}", e); - None + let browse_failed = + self.daemon_proxy + .receive_browse_failed() + .await? + .filter_map(|s| async move { + match s.args() { + Ok(args) => Some(ServiceEvent::BrowseFailed( + args.device_id.clone(), + args.message.clone(), + )), + Err(e) => { + error!("Failed to parse BrowseFailed signal: {:?}", e); + None + } } - } - }); + }); let mount_state = self .daemon_proxy diff --git a/kdeconnect-service/src/dbus_interface.rs b/kdeconnect-service/src/dbus_interface.rs index 00bcee10..64fba862 100755 --- a/kdeconnect-service/src/dbus_interface.rs +++ b/kdeconnect-service/src/dbus_interface.rs @@ -44,7 +44,8 @@ pub struct DbusDevice { fn device_cache_dir(device_id: &str) -> std::path::PathBuf { let base = dirs::data_local_dir().unwrap_or_else(|| std::path::PathBuf::from("~/.local/share")); - base.join(kdeconnect_core::config::CONFIG_DIR).join(device_id) + base.join(kdeconnect_core::config::CONFIG_DIR) + .join(device_id) } async fn save_contacts_cache(device_id: &str, contacts: &HashMap) { @@ -363,7 +364,10 @@ impl DaemonInterface { .map(|d| d.name.clone()); if let Some(name) = device_name.as_deref() { if kdeconnect_core::plugins::sftp::open_mounted(&device_id, name).await { - debug!("BrowseDevice: {} already mounted, opened directly", device_id); + debug!( + "BrowseDevice: {} already mounted, opened directly", + device_id + ); return Ok(()); } } @@ -456,7 +460,9 @@ impl DaemonInterface { async fn accept_pairing(&self, device_id: String) -> zbus::fdo::Result<()> { info!("D-Bus: AcceptPairing called for {}", device_id); self.event_sender - .send(AppEvent::AcceptPairing(kdeconnect_core::device::DeviceId(device_id))) + .send(AppEvent::AcceptPairing(kdeconnect_core::device::DeviceId( + device_id, + ))) .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; Ok(()) } @@ -465,7 +471,9 @@ impl DaemonInterface { async fn reject_pairing(&self, device_id: String) -> zbus::fdo::Result<()> { info!("D-Bus: RejectPairing called for {}", device_id); self.event_sender - .send(AppEvent::RejectPairing(kdeconnect_core::device::DeviceId(device_id))) + .send(AppEvent::RejectPairing(kdeconnect_core::device::DeviceId( + device_id, + ))) .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; Ok(()) } @@ -553,10 +561,7 @@ impl DaemonInterface { /// Execute a remote command on a device by key async fn run_command(&self, device_id: String, key: String) -> zbus::fdo::Result<()> { info!("D-Bus: RunCommand called for {} key={}", device_id, key); - let packet = ProtocolPacket::new( - PacketType::RunCommandRequest, - json!({ "key": key }), - ); + let packet = ProtocolPacket::new(PacketType::RunCommandRequest, json!({ "key": key })); self.event_sender .send(AppEvent::SendPacket(DeviceId(device_id), packet)) .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; @@ -581,7 +586,9 @@ impl DaemonInterface { async fn push_local_commands(&self, device_id: String) -> zbus::fdo::Result<()> { info!("D-Bus: PushLocalCommands called for {}", device_id); self.event_sender - .send(AppEvent::PushLocalCommands(kdeconnect_core::device::DeviceId(device_id))) + .send(AppEvent::PushLocalCommands( + kdeconnect_core::device::DeviceId(device_id), + )) .map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?; Ok(()) } @@ -664,14 +671,13 @@ impl SmsInterface { ) -> zbus::fdo::Result<()> { info!( "D-Bus: SendSms called for {} to {} ({} attachment(s))", - device_id, phone_number, attachments.len() + device_id, + phone_number, + attachments.len() ); - let packet = kdeconnect_core::plugins::sms::build_send_packet( - &phone_number, - &message, - &attachments, - ) - .await; + let packet = + kdeconnect_core::plugins::sms::build_send_packet(&phone_number, &message, &attachments) + .await; self.event_sender .send(AppEvent::SendPacket(DeviceId(device_id), packet)) .map_err(|e| { @@ -805,15 +811,14 @@ impl KdeConnectService { let sms_cache = self.sms_cache.clone(); let clipboard = self.clipboard.clone(); tokio::spawn(async move { - if let Err(e) = - crate::varlink_server::run_varlink_server( - event_sender, - devices, - sms_cache, - clipboard, - broadcast_tx, - ) - .await + if let Err(e) = crate::varlink_server::run_varlink_server( + event_sender, + devices, + sms_cache, + clipboard, + broadcast_tx, + ) + .await { warn!("Varlink server exited: {:?}", e); } @@ -999,10 +1004,9 @@ impl KdeConnectService { PacketType::Clipboard, json!({ "content": content.text.clone() }), ); - if let Err(error) = event_sender.send(AppEvent::SendPacket( - DeviceId(device.id.clone()), - packet, - )) { + if let Err(error) = event_sender + .send(AppEvent::SendPacket(DeviceId(device.id.clone()), packet)) + { error!( "Failed to queue automatic clipboard for {}: {error}", device.id @@ -1022,7 +1026,9 @@ impl KdeConnectService { tokio::spawn(async move { match core_handle.await { Ok(_) => error!("Core event loop exited unexpectedly - connections will fail"), - Err(e) if e.is_panic() => error!("Core event loop PANICKED - connections will fail: {:?}", e), + Err(e) if e.is_panic() => { + error!("Core event loop PANICKED - connections will fail: {:?}", e) + } Err(e) => error!("Core event loop cancelled: {:?}", e), } }); @@ -1246,8 +1252,11 @@ impl KdeConnectService { .interface::<_, DaemonInterface>(DAEMON_PATH) .await?; - DaemonInterface::device_disconnected(iface_ref.signal_emitter(), device_id.0.clone()) - .await?; + DaemonInterface::device_disconnected( + iface_ref.signal_emitter(), + device_id.0.clone(), + ) + .await?; debug!("Device disconnected signal emitted"); let _ = broadcast_tx.send(crate::varlink_server::VarlinkEvent { @@ -1257,7 +1266,10 @@ impl KdeConnectService { }); } ConnectionEvent::PairStateChanged((device_id, pair_state)) => { - info!("Event: PairStateChanged - {} → {:?}", device_id.0, pair_state); + info!( + "Event: PairStateChanged - {} → {:?}", + device_id.0, pair_state + ); let is_paired = matches!(pair_state, PairState::Paired); { @@ -1452,14 +1464,18 @@ impl KdeConnectService { info!( "Accepted clipboard.connect ({} bytes, remote timestamp {:?}, local timestamp {})", - content.len(), timestamp, local_timestamp + content.len(), + timestamp, + local_timestamp ); if let Some(clipboard) = clipboard { if let Err(error) = clipboard.set_text(content.clone()) { error!("Failed to write connected device clipboard to desktop: {error}"); } } else { - error!("Cannot write connected device clipboard: background clipboard access unavailable"); + error!( + "Cannot write connected device clipboard: background clipboard access unavailable" + ); } DaemonInterface::clipboard_received(iface_ref.signal_emitter(), content.clone()) diff --git a/kdeconnect-service/src/main.rs b/kdeconnect-service/src/main.rs index 94a205e1..e6057f1c 100755 --- a/kdeconnect-service/src/main.rs +++ b/kdeconnect-service/src/main.rs @@ -20,8 +20,7 @@ async fn main() -> Result<()> { if std::env::var("KDECONNECT_LOG_FILE").is_ok() && std::path::Path::new("/.flatpak-info").exists() { - let log_dir = dirs::data_dir() - .unwrap_or_else(|| std::path::PathBuf::from("/tmp")); + let log_dir = dirs::data_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); let _ = std::fs::create_dir_all(&log_dir); let file = std::fs::OpenOptions::new() .create(true) diff --git a/kdeconnect-service/src/varlink_server.rs b/kdeconnect-service/src/varlink_server.rs index e99cefe5..d5033949 100644 --- a/kdeconnect-service/src/varlink_server.rs +++ b/kdeconnect-service/src/varlink_server.rs @@ -2,27 +2,25 @@ use anyhow::Result; use async_trait::async_trait; +use kdeconnect_core::{PacketType, ProtocolPacket, device::DeviceId, event::AppEvent}; use kdeconnect_varlink::iface::{ - self, BatteryState, Device, VarlinkInterface, - Call_ListDevices, Call_PairDevice, Call_UnpairDevice, Call_SendPing, - Call_SendFiles, Call_SendClipboard, Call_ShareClipboard, Call_RunCommand, - Call_RingDevice, Call_BrowseDevice, Call_UnmountDevice, Call_MountedDevices, - Call_BroadcastIdentity, Call_RequestRunCommands, - Call_SetPluginEnabled, Call_GetPluginEnabled, Call_GetDisabledPlugins, - Call_AcceptPairing, Call_RejectPairing, Call_Subscribe, - Call_RequestConversations, Call_RequestConversation, Call_SendSms, - Call_GetCachedSms, Call_RequestContacts, Call_GetCachedContacts, Call_RequestSmsAttachment, - Call_GetCachedContactPhotos, + self, BatteryState, Call_AcceptPairing, Call_BroadcastIdentity, Call_BrowseDevice, + Call_GetCachedContactPhotos, Call_GetCachedContacts, Call_GetCachedSms, + Call_GetDisabledPlugins, Call_GetPluginEnabled, Call_ListDevices, Call_MountedDevices, + Call_PairDevice, Call_RejectPairing, Call_RequestContacts, Call_RequestConversation, + Call_RequestConversations, Call_RequestRunCommands, Call_RequestSmsAttachment, Call_RingDevice, + Call_RunCommand, Call_SendClipboard, Call_SendFiles, Call_SendPing, Call_SendSms, + Call_SetPluginEnabled, Call_ShareClipboard, Call_Subscribe, Call_UnmountDevice, + Call_UnpairDevice, Device, VarlinkInterface, }; use kdeconnect_varlink::socket_address; -use kdeconnect_core::{PacketType, ProtocolPacket, device::DeviceId, event::AppEvent}; use serde_json::json; use std::sync::Arc; use tokio::sync::{broadcast, mpsc}; -use varlink::{listen_async, ListenAsyncConfig}; +use varlink::{ListenAsyncConfig, listen_async}; -use crate::dbus_interface::DbusDevice; use crate::clipboard::ClipboardHandle; +use crate::dbus_interface::DbusDevice; // DORMANT: built and broadcast on every device/battery/connectivity/clipboard/ // pairing/run-command event in dbus_interface.rs, but nothing can actually @@ -57,7 +55,13 @@ impl KdeConnectVarlinkService { clipboard: Option, broadcast_tx: broadcast::Sender, ) -> Self { - Self { event_sender, devices, sms_cache, clipboard, broadcast_tx } + Self { + event_sender, + devices, + sms_cache, + clipboard, + broadcast_tx, + } } } @@ -79,27 +83,56 @@ impl VarlinkInterface for KdeConnectVarlinkService { call.reply(devices) } - async fn pair_device(&self, call: &mut dyn Call_PairDevice, device_id: String) -> varlink::Result<()> { + async fn pair_device( + &self, + call: &mut dyn Call_PairDevice, + device_id: String, + ) -> varlink::Result<()> { let _ = self.event_sender.send(AppEvent::Pair(DeviceId(device_id))); call.reply() } - async fn unpair_device(&self, call: &mut dyn Call_UnpairDevice, device_id: String) -> varlink::Result<()> { - let _ = self.event_sender.send(AppEvent::Unpair(DeviceId(device_id))); + async fn unpair_device( + &self, + call: &mut dyn Call_UnpairDevice, + device_id: String, + ) -> varlink::Result<()> { + let _ = self + .event_sender + .send(AppEvent::Unpair(DeviceId(device_id))); call.reply() } - async fn send_ping(&self, call: &mut dyn Call_SendPing, device_id: String, message: String) -> varlink::Result<()> { - let _ = self.event_sender.send(AppEvent::Ping((DeviceId(device_id), message))); + async fn send_ping( + &self, + call: &mut dyn Call_SendPing, + device_id: String, + message: String, + ) -> varlink::Result<()> { + let _ = self + .event_sender + .send(AppEvent::Ping((DeviceId(device_id), message))); call.reply() } - async fn send_files(&self, call: &mut dyn Call_SendFiles, device_id: String, files: Vec) -> varlink::Result<()> { - let _ = self.event_sender.send(AppEvent::SendFiles((DeviceId(device_id), files))); + async fn send_files( + &self, + call: &mut dyn Call_SendFiles, + device_id: String, + files: Vec, + ) -> varlink::Result<()> { + let _ = self + .event_sender + .send(AppEvent::SendFiles((DeviceId(device_id), files))); call.reply() } - async fn send_clipboard(&self, call: &mut dyn Call_SendClipboard, device_id: String, content: String) -> varlink::Result<()> { + async fn send_clipboard( + &self, + call: &mut dyn Call_SendClipboard, + device_id: String, + content: String, + ) -> varlink::Result<()> { match crate::dbus_interface::send_clipboard_packet( &self.event_sender, &self.devices, @@ -125,9 +158,8 @@ impl VarlinkInterface for KdeConnectVarlinkService { ); }; let Some(content) = clipboard.current() else { - return call.reply_service_error( - "The current clipboard does not contain text".to_string(), - ); + return call + .reply_service_error("The current clipboard does not contain text".to_string()); }; match crate::dbus_interface::send_clipboard_packet( &self.event_sender, @@ -142,33 +174,61 @@ impl VarlinkInterface for KdeConnectVarlinkService { } } - async fn run_command(&self, call: &mut dyn Call_RunCommand, device_id: String, key: String) -> varlink::Result<()> { + async fn run_command( + &self, + call: &mut dyn Call_RunCommand, + device_id: String, + key: String, + ) -> varlink::Result<()> { let packet = ProtocolPacket::new(PacketType::RunCommandRequest, json!({ "key": key })); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } - async fn ring_device(&self, call: &mut dyn Call_RingDevice, device_id: String) -> varlink::Result<()> { + async fn ring_device( + &self, + call: &mut dyn Call_RingDevice, + device_id: String, + ) -> varlink::Result<()> { let packet = ProtocolPacket::new(PacketType::FindMyPhoneRequest, json!({})); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } - async fn browse_device(&self, call: &mut dyn Call_BrowseDevice, device_id: String) -> varlink::Result<()> { + async fn browse_device( + &self, + call: &mut dyn Call_BrowseDevice, + device_id: String, + ) -> varlink::Result<()> { // Fast path mirroring the D-Bus BrowseDevice: already mounted and // healthy → just reopen the file manager, skip the phone round-trip. - let device_name = self.devices.lock().await.get(&device_id).map(|d| d.name.clone()); + let device_name = self + .devices + .lock() + .await + .get(&device_id) + .map(|d| d.name.clone()); if let Some(name) = device_name.as_deref() { if kdeconnect_core::plugins::sftp::open_mounted(&device_id, name).await { return call.reply(); } } let packet = ProtocolPacket::new(PacketType::SftpRequest, json!({ "startBrowsing": true })); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } - async fn unmount_device(&self, call: &mut dyn Call_UnmountDevice, device_id: String) -> varlink::Result<()> { + async fn unmount_device( + &self, + call: &mut dyn Call_UnmountDevice, + device_id: String, + ) -> varlink::Result<()> { let device_name = self .devices .lock() @@ -193,20 +253,35 @@ impl VarlinkInterface for KdeConnectVarlinkService { call.reply(kdeconnect_core::plugins::sftp::mounted_devices(&pairs).await) } - async fn broadcast_identity(&self, call: &mut dyn Call_BroadcastIdentity) -> varlink::Result<()> { + async fn broadcast_identity( + &self, + call: &mut dyn Call_BroadcastIdentity, + ) -> varlink::Result<()> { let _ = self.event_sender.send(AppEvent::Broadcasting); call.reply() } - async fn request_run_commands(&self, call: &mut dyn Call_RequestRunCommands, device_id: String) -> varlink::Result<()> { - let packet = ProtocolPacket::new(PacketType::RunCommandRequest, json!({ "requestCommandList": true })); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + async fn request_run_commands( + &self, + call: &mut dyn Call_RequestRunCommands, + device_id: String, + ) -> varlink::Result<()> { + let packet = ProtocolPacket::new( + PacketType::RunCommandRequest, + json!({ "requestCommandList": true }), + ); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } async fn set_plugin_enabled( - &self, call: &mut dyn Call_SetPluginEnabled, - device_id: String, plugin: String, enabled: bool, + &self, + call: &mut dyn Call_SetPluginEnabled, + device_id: String, + plugin: String, + enabled: bool, ) -> varlink::Result<()> { let _ = self.event_sender.send(AppEvent::SetPluginEnabled { device_id: DeviceId(device_id), @@ -217,71 +292,123 @@ impl VarlinkInterface for KdeConnectVarlinkService { } async fn get_plugin_enabled( - &self, call: &mut dyn Call_GetPluginEnabled, - device_id: String, plugin: String, + &self, + call: &mut dyn Call_GetPluginEnabled, + device_id: String, + plugin: String, ) -> varlink::Result<()> { let disabled = kdeconnect_core::plugin_config::load_disabled_plugins(&device_id).await; call.reply(!disabled.contains(&plugin)) } async fn get_disabled_plugins( - &self, call: &mut dyn Call_GetDisabledPlugins, + &self, + call: &mut dyn Call_GetDisabledPlugins, device_id: String, ) -> varlink::Result<()> { let disabled = kdeconnect_core::plugin_config::load_disabled_plugins(&device_id).await; call.reply(disabled.into_iter().collect()) } - async fn accept_pairing(&self, call: &mut dyn Call_AcceptPairing, device_id: String) -> varlink::Result<()> { - let _ = self.event_sender.send(AppEvent::AcceptPairing(DeviceId(device_id))); + async fn accept_pairing( + &self, + call: &mut dyn Call_AcceptPairing, + device_id: String, + ) -> varlink::Result<()> { + let _ = self + .event_sender + .send(AppEvent::AcceptPairing(DeviceId(device_id))); call.reply() } - async fn reject_pairing(&self, call: &mut dyn Call_RejectPairing, device_id: String) -> varlink::Result<()> { - let _ = self.event_sender.send(AppEvent::RejectPairing(DeviceId(device_id))); + async fn reject_pairing( + &self, + call: &mut dyn Call_RejectPairing, + device_id: String, + ) -> varlink::Result<()> { + let _ = self + .event_sender + .send(AppEvent::RejectPairing(DeviceId(device_id))); call.reply() } - async fn request_conversations(&self, call: &mut dyn Call_RequestConversations, device_id: String) -> varlink::Result<()> { + async fn request_conversations( + &self, + call: &mut dyn Call_RequestConversations, + device_id: String, + ) -> varlink::Result<()> { let packet = ProtocolPacket::new(PacketType::SmsRequestConversations, json!({})); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } async fn request_conversation( - &self, call: &mut dyn Call_RequestConversation, - device_id: String, thread_id: i64, + &self, + call: &mut dyn Call_RequestConversation, + device_id: String, + thread_id: i64, ) -> varlink::Result<()> { - let packet = ProtocolPacket::new(PacketType::SmsRequestConversation, json!({ "threadID": thread_id })); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let packet = ProtocolPacket::new( + PacketType::SmsRequestConversation, + json!({ "threadID": thread_id }), + ); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } async fn send_sms( - &self, call: &mut dyn Call_SendSms, - device_id: String, phone_number: String, message: String, attachments: Vec, + &self, + call: &mut dyn Call_SendSms, + device_id: String, + phone_number: String, + message: String, + attachments: Vec, ) -> varlink::Result<()> { let packet = kdeconnect_core::plugins::sms::build_send_packet(&phone_number, &message, &attachments) .await; - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } - async fn get_cached_sms(&self, call: &mut dyn Call_GetCachedSms, device_id: String) -> varlink::Result<()> { + async fn get_cached_sms( + &self, + call: &mut dyn Call_GetCachedSms, + device_id: String, + ) -> varlink::Result<()> { if let Some(json) = self.sms_cache.lock().await.as_ref() { return call.reply(json.clone()); } - call.reply(crate::dbus_interface::load_sms_cache(&device_id).await.unwrap_or_default()) + call.reply( + crate::dbus_interface::load_sms_cache(&device_id) + .await + .unwrap_or_default(), + ) } - async fn request_contacts(&self, call: &mut dyn Call_RequestContacts, device_id: String) -> varlink::Result<()> { + async fn request_contacts( + &self, + call: &mut dyn Call_RequestContacts, + device_id: String, + ) -> varlink::Result<()> { let packet = ProtocolPacket::new(PacketType::ContactsRequestAllUidsTimestamps, json!({})); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } - async fn get_cached_contacts(&self, call: &mut dyn Call_GetCachedContacts, device_id: String) -> varlink::Result<()> { + async fn get_cached_contacts( + &self, + call: &mut dyn Call_GetCachedContacts, + device_id: String, + ) -> varlink::Result<()> { let json = match crate::dbus_interface::load_contacts_cache(&device_id).await { Some(contacts) => serde_json::to_string(&contacts).unwrap_or_else(|_| "{}".to_string()), None => "{}".to_string(), @@ -290,7 +417,9 @@ impl VarlinkInterface for KdeConnectVarlinkService { } async fn get_cached_contact_photos( - &self, call: &mut dyn Call_GetCachedContactPhotos, device_id: String, + &self, + call: &mut dyn Call_GetCachedContactPhotos, + device_id: String, ) -> varlink::Result<()> { let json = match crate::dbus_interface::load_contact_photos_cache(&device_id).await { Some(photos) => serde_json::to_string(&photos).unwrap_or_else(|_| "{}".to_string()), @@ -300,14 +429,19 @@ impl VarlinkInterface for KdeConnectVarlinkService { } async fn request_sms_attachment( - &self, call: &mut dyn Call_RequestSmsAttachment, - device_id: String, part_id: i64, unique_identifier: String, + &self, + call: &mut dyn Call_RequestSmsAttachment, + device_id: String, + part_id: i64, + unique_identifier: String, ) -> varlink::Result<()> { let packet = ProtocolPacket::new( PacketType::SmsRequestAttachment, json!({ "part_id": part_id, "unique_identifier": unique_identifier }), ); - let _ = self.event_sender.send(AppEvent::SendPacket(DeviceId(device_id), packet)); + let _ = self + .event_sender + .send(AppEvent::SendPacket(DeviceId(device_id), packet)); call.reply() } @@ -326,7 +460,9 @@ impl VarlinkInterface for KdeConnectVarlinkService { match rx.recv().await { Ok(ev) => { let device = ev.device.as_ref().map(to_varlink_device); - let battery = ev.battery.map(|(level, is_charging)| BatteryState { level, is_charging }); + let battery = ev + .battery + .map(|(level, is_charging)| BatteryState { level, is_charging }); call.reply( ev.event_type, ev.device_id, @@ -362,12 +498,7 @@ pub async fn run_varlink_server( )); let handler = Arc::new(iface::new(service)); - listen_async( - handler, - &socket_address(), - &ListenAsyncConfig::default(), - ) - .await?; + listen_async(handler, &socket_address(), &ListenAsyncConfig::default()).await?; Ok(()) } From a17705e44a10052bceb24c4e4674c4483fdf9c78 Mon Sep 17 00:00:00 2001 From: Piotr Tomaszewski Date: Sat, 5 Sep 2026 15:37:45 +0200 Subject: [PATCH 2/2] wip: design home page aka dashboard --- cosmic-ext-connect-applet/src/main.rs | 1 - cosmic-ext-connect-applet/src/ui/popup.rs | 259 ++++++++++------------ 2 files changed, 119 insertions(+), 141 deletions(-) diff --git a/cosmic-ext-connect-applet/src/main.rs b/cosmic-ext-connect-applet/src/main.rs index 9395d7f7..c05c6e4a 100755 --- a/cosmic-ext-connect-applet/src/main.rs +++ b/cosmic-ext-connect-applet/src/main.rs @@ -490,7 +490,6 @@ impl cosmic::Application for KdeConnectApplet { &self.devices, self.expanded_device.as_ref(), Some(&self.pairing_requests), - &self.unread_sms, self.error_banner.as_ref(), &self.now_playing, ); diff --git a/cosmic-ext-connect-applet/src/ui/popup.rs b/cosmic-ext-connect-applet/src/ui/popup.rs index a56c8754..debb5aee 100755 --- a/cosmic-ext-connect-applet/src/ui/popup.rs +++ b/cosmic-ext-connect-applet/src/ui/popup.rs @@ -3,7 +3,7 @@ use crate::models::{Device, NowPlaying}; use cosmic::app::Core; use cosmic::iced::core::text::Wrapping; use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{Row, icon, settings, space::horizontal, text}; +use cosmic::widget::{Column, Row, icon, settings, space::horizontal, text}; use cosmic::{Element, theme, widget}; use std::collections::HashMap; @@ -16,7 +16,6 @@ pub fn create_popup_view<'a>( devices: &'a HashMap, expanded_device: Option<&'a String>, pairing_requests: Option<&'a HashMap>, - unread_sms: &'a HashMap, error_banner: Option<&'a String>, now_playing: &'a HashMap, ) -> Element<'a, Message> { @@ -137,26 +136,24 @@ pub fn create_popup_view<'a>( if paired_devices.is_empty() { content = content.push( - widget::container(widget::text(fl!("devices-none-paired")).size(14)) + widget::container(widget::text(fl!("devices-none-paired")).size(15)) .padding(spacing.space_m) .width(Length::Fill) .center_x(Length::Fill), ); } else { content = content.push( - widget::text(fl!("devices-header")) - .size(14) - .font(cosmic::font::bold()), + widget::container( + widget::text(fl!("devices-header")) + .font(cosmic::font::bold()) + .size(15), + ) + .padding(spacing.space_xs) + .width(Length::Fill), ); for device in paired_devices { - let device_unread = unread_sms.get(&device.id).copied().unwrap_or(false); - content = content.push(create_device_card( - device, - &spacing, - expanded_device, - device_unread, - )); + content = content.push(create_device_card(device, &spacing, expanded_device)); } } @@ -196,34 +193,52 @@ fn create_device_card<'a>( device: &'a Device, spacing: &cosmic::cosmic_theme::Spacing, expanded_device: Option<&'a String>, - has_unread_sms: bool, ) -> Element<'a, Message> { let is_expanded = expanded_device == Some(&device.id); let is_online = device.is_reachable; - let mut name_row = Row::new() - .push(widget::icon::from_name(device.device_icon()).size(20)) - .push(widget::text(&device.name).size(14).width(Length::Fill)) - .spacing(spacing.space_xs) - .align_y(Alignment::Center); + let mut quick_actions_list = widget::list_column(); + + let mut menu_items = widget::Column::new(); + let mut name_row = Row::new(); + + let phone_icon = widget::icon::from_name("smartphone-symbolic").size(42); + + name_row = name_row.push(phone_icon); + + let mut name_col = Column::new() + .spacing(spacing.space_s) + .padding(spacing.space_xxs) + .push( + widget::text(&device.name) + .size(15) + .font(cosmic::font::bold()) + .width(Length::Fill), + ); if !is_online { - name_row = name_row.push(widget::text(fl!("devices-offline")).size(11)); + name_col = name_col.push(widget::text::title4(fl!("devices-offline")).size(12)); } else { - if let Some(signal_icon) = device.signal_icon() { - name_row = name_row.push(widget::icon::from_name(signal_icon).size(16)); - } + let mut under_row = widget::Row::new().spacing(spacing.space_xs); + if let Some(level) = device.battery_level { - name_row = name_row.push( + under_row = under_row.push( Row::new() - .spacing(2) + .spacing(8) .align_y(Alignment::Center) .push(widget::icon::from_name(device.battery_icon()).size(16)) .push(widget::text(format!("{}%", level)).size(11)), ); } + + if let Some(signal_icon) = device.signal_icon() { + under_row = under_row.push(widget::icon::from_name(signal_icon).size(16)); + } + name_col = name_col.push(under_row); } + name_row = name_row.push(name_col).align_y(Alignment::Center); + name_row = name_row.push( widget::button::icon(widget::icon::from_name(if is_expanded { "go-up-symbolic" @@ -234,98 +249,59 @@ fn create_device_card<'a>( .class(cosmic::theme::Button::Icon), ); - let device_button = widget::button::custom(name_row) - .on_press(Message::ToggleDeviceMenu(device.id.clone())) - .width(Length::Fill); - - let mut col = widget::Column::new() - .width(Length::Fill) - .push(device_button); + quick_actions_list = quick_actions_list.add(name_row); + + let quick_action_btn = + |icon: &str, action: String, msg: messages::Message| -> Element<'_, Message> { + widget::button::custom(settings::item_row(vec![ + icon::from_name(icon).size(16).icon().into(), + text::body(action) + .width(Length::Fill) + .wrapping(Wrapping::Word) + .into(), + ])) + .width(Length::Fill) + .on_press(msg) + .class(theme::Button::Link) + .into() + }; if is_expanded && is_online { - let mut menu_items = widget::Column::new().spacing(spacing.space_xxs); - - let mut quick_actions_list = - widget::list_column().style(cosmic::theme::Container::Transparent); - - quick_actions_list = - quick_actions_list.add(widget::text::caption_heading(fl!("quick-actions-header"))); - - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-ping")).class(cosmic::theme::Text::Accent), - ) - .on_press(Message::PingDevice(device.id.clone())) - .class(cosmic::theme::Button::Link), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "notification-new-symbolic", + fl!("quick-actions-ping"), + Message::PingDevice(device.id.clone()), + )); if device.has_findmyphone { - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-find-phone")) - .class(cosmic::theme::Text::Accent), - ) - .on_press(Message::RingDevice(device.id.clone())) - .class(cosmic::theme::Button::Link), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "phone-symbolic", + fl!("quick-actions-find-phone"), + Message::RingDevice(device.id.clone()), + )); } if device.has_clipboard { - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-share-clipboard")) - .class(cosmic::theme::Text::Accent), - ) - .on_press(Message::ShareClipboard(device.id.clone())) - .class(cosmic::theme::Button::Link), - ); - } - - let mut sms_label = Row::new() - .push(widget::text::caption_heading(fl!("quick-actions-sms"))) - .align_y(Alignment::Center) - .spacing(spacing.space_xs); - - if has_unread_sms { - sms_label = sms_label.push( - widget::container( - widget::Space::new() - .width(Length::Fixed(8.0)) - .height(Length::Fixed(8.0)), - ) - .class(cosmic::theme::Container::custom(move |_theme| { - cosmic::iced::widget::container::Style { - border: cosmic::iced::Border { - radius: cosmic::iced::Radius::from(4.0), - ..Default::default() - }, - ..Default::default() - } - })), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "edit-paste-symbolic", + fl!("quick-actions-share-clipboard"), + Message::ShareClipboard(device.id.clone()), + )); } - quick_actions_list = quick_actions_list.add( - widget::button::custom(sms_label) - .on_press(Message::SendSMS(device.id.clone())) - .class(cosmic::theme::Button::Standard) - .width(Length::Fill), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "mail-message-new-symbolic", + fl!("quick-actions-sms"), + Message::SendSMS(device.id.clone()), + )); if device.has_share || device.has_sftp { - quick_actions_list = quick_actions_list.add(widget::text::caption_heading(fl!( - "quick-actions-files-header" - ))); - if device.has_share { - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-send-file")) - .class(cosmic::theme::Text::Accent), - ) - .on_press(Message::SendFiles(device.id.clone())) - .class(cosmic::theme::Button::Link), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "document-send-symbolic", + fl!("quick-actions-send-file"), + Message::SendFiles(device.id.clone()), + )); if device.share_progress.is_some_and(|p| p > 0) { quick_actions_list = @@ -336,55 +312,58 @@ fn create_device_card<'a>( } if device.has_sftp { - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-browse-device")) - .class(cosmic::theme::Text::Accent), - ) - .on_press(Message::BrowseDevice(device.id.clone())) - .class(cosmic::theme::Button::Link), + let mut item_row = Vec::with_capacity(4); + item_row.push( + icon::from_name(if !(device.is_mounted) { + "folder-symbolic" + } else { + "folder-open-symbolic" + }) + .size(16) + .icon() + .into(), ); + item_row.push( + text::body(fl!("quick-actions-browse-device")) + .width(Length::Fill) + .wrapping(Wrapping::Word) + .into(), + ); + if device.is_mounted { - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(fl!("quick-actions-unmount-device")) - .class(cosmic::theme::Text::Accent), - ) - .on_press(Message::UnmountDevice(device.id.clone())) - .class(cosmic::theme::Button::Link), + item_row.push(horizontal().into()); + item_row.push( + widget::button::icon(icon::from_name("media-eject-symbolic")) + .on_press(Message::UnmountDevice(device.id.clone())) + .class(cosmic::theme::Button::Link) + .into(), ); } + + quick_actions_list = quick_actions_list.add( + widget::button::custom(settings::item_row(item_row)) + .width(Length::Fill) + .on_press(Message::BrowseDevice(device.id.clone())) + .class(theme::Button::Link), + ); } } if !device.run_commands.is_empty() { - quick_actions_list = quick_actions_list.add(widget::text::caption_heading(fl!( - "quick-actions-run-commands-header" - ))); for (key, name) in &device.run_commands { let key = key.clone(); - quick_actions_list = quick_actions_list.add( - widget::button::custom( - widget::text::caption(name.as_str()).class(cosmic::theme::Text::Accent), - ) - .on_press(Message::ExecuteRunCommand(device.id.clone(), key)) - .class(cosmic::theme::Button::Link), - ); + quick_actions_list = quick_actions_list.add(quick_action_btn( + "system-run-symbolic", + name.to_owned(), + Message::ExecuteRunCommand(device.id.clone(), key), + )); } } - - menu_items = menu_items.push(quick_actions_list); - - col = col.push(widget::container(menu_items).padding([spacing.space_xs, spacing.space_m])); - } else if is_expanded && !is_online { - col = col.push( - widget::container(widget::text(fl!("devices-not-reachable")).size(12)) - .padding([spacing.space_xs, spacing.space_m]) - .class(cosmic::theme::Container::Card), - ); } - col.into() + menu_items = menu_items.push(quick_actions_list); + + menu_items.into() } /// One card per active phone media player: album art (if downloaded),