diff --git a/CHANGES b/CHANGES index bfafc6a..58eff22 100644 --- a/CHANGES +++ b/CHANGES @@ -23,3 +23,6 @@ - Fallback to finding user with libc instead of enviroment variable - Swedish translation thanks to @bittin - Finnish translation +0.3.11: + - Implement a confirmation dialog for "Clear Device" operation + - Fix missing bulk deletion logic for all fingerprints of a user diff --git a/Cargo.lock b/Cargo.lock index 596a360..e692668 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,7 +1020,7 @@ dependencies = [ [[package]] name = "cosmic-ext-fprint" -version = "0.3.10" +version = "0.3.11" dependencies = [ "futures-util", "i18n-embed 0.15.4", diff --git a/Cargo.toml b/Cargo.toml index a75fcf1..aab6fa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-ext-fprint" -version = "0.3.10" +version = "0.3.11" edition = "2024" license = "MPL-2.0" description = "GUI for fprintd fingerprint enrolling" diff --git a/flatpak/fi.joonastuomi.Fprint.yml b/flatpak/fi.joonastuomi.Fprint.yml index 4449570..14849b9 100644 --- a/flatpak/fi.joonastuomi.Fprint.yml +++ b/flatpak/fi.joonastuomi.Fprint.yml @@ -33,10 +33,10 @@ modules: - install -Dm0755 target/release/cosmic-ext-fprint /app/bin/cosmic-ext-fprint # Install the desktop file - - install -Dm0644 resources/app.desktop /app/share/applications/fi.joonastuomi.Fprint.desktop + - install -Dm0644 resources/fi.joonastuomi.Fprint.desktop /app/share/applications/fi.joonastuomi.Fprint.desktop # Install the AppStream metadata - - install -Dm0644 resources/app.metainfo.xml /app/share/appdata/fi.joonastuomi.Fprint.metainfo.xml + - install -Dm0644 resources/fi.joonastuomi.Fprint.metainfo.xml /app/share/appdata/fi.joonastuomi.Fprint.metainfo.xml # Install the icon - install -Dm0644 resources/icons/hicolor/scalable/apps/icon.svg /app/share/icons/hicolor/scalable/apps/fi.joonastuomi.Fprint.svg diff --git a/i18n/en/cosmic_ext_fprint.ftl b/i18n/en/cosmic_ext_fprint.ftl index 96b4dc4..e20dfd1 100644 --- a/i18n/en/cosmic_ext_fprint.ftl +++ b/i18n/en/cosmic_ext_fprint.ftl @@ -6,8 +6,13 @@ welcome = Register and/or delete fingerprints git-description = Git commit {$hash} on {$date} register = Register delete = Delete -deleting = Deleting fingerprint... deleted = Deleted fingerprint. +deleting = Deleting fingerprint... +clear-device = Clear Device +confirm-clear = Are you sure? +clearing-device = Clearing all fingerprints from device for all known users... +device-cleared = Device cleared for all known users. +clear-device-confirm = Are you sure you want to clear fingerprints for ALL known users? cancel = Cancel page-right-thumb = Right Thumb @@ -20,6 +25,7 @@ page-left-index-finger = Left Index Finger page-left-middle-finger = Left Middle Finger page-left-ring-finger = Left Ring Finger page-left-little-finger = Left Little Finger +page-delete-all-users-prints = Delete All User's Prints status-connecting = Connecting to system bus... status-searching-device = Searching for fingerprint reader... diff --git a/i18n/fi/cosmic_ext_fprint.ftl b/i18n/fi/cosmic_ext_fprint.ftl index 33ca288..edbc4ec 100644 --- a/i18n/fi/cosmic_ext_fprint.ftl +++ b/i18n/fi/cosmic_ext_fprint.ftl @@ -6,8 +6,13 @@ welcome = Rekisteröi tai poista sormenjälkiä git-description = Git julkaisu {$hash} päivänä {$date} register = Rekisteröi delete = Poista -deleting = Poistetaan sormenjälkeä... deleted = Sormenjälki poistettu. +deleting = Poistetaan sormenjälkeä... +clear-device = Tyhjennä laite +confirm-clear = Oletko varma? +clearing-device = Tyhjennetään laitteen kaikki käyttäjien sormenjäljet... +device-cleared = Laite tyhjennetty. +clear-device-confirm = Oletko varma, että haluat poistaa kaikki käyttäjien sormenjäljet? cancel = Peruuta page-right-thumb = Oikea Peukalo @@ -20,6 +25,7 @@ page-left-index-finger = Vasen Etusormi page-left-middle-finger = Vasen Keskisormi page-left-ring-finger = Vasen Nimetön page-left-little-finger = Vasen Pikkusormi +page-delete-all-users-prints = Poista kaikki käyttäjien sormenjäljet status-connecting = Yhdistetään järjestelmäväylään... status-searching-device = Etsitään sormenjälkilukijaa... diff --git a/i18n/sv/cosmic_ext_fprint.ftl b/i18n/sv/cosmic_ext_fprint.ftl index dd1d628..f5508a8 100644 --- a/i18n/sv/cosmic_ext_fprint.ftl +++ b/i18n/sv/cosmic_ext_fprint.ftl @@ -6,8 +6,13 @@ welcome = Registrera och/eller radera fingeravtryck git-description = Git commit {$hash} på {$date} register = Registrera delete = Radera -deleting = Raderar fingeravtryck... deleted = Raderat fingeravtryck. +deleting = Raderar fingeravtryck... +clear-device = Rensa enhet +confirm-clear = Är du säker? +clearing-device = Rensar enhet... +device-cleared = Rensad för alla kända användare. +clear-device-confirm = Är du säker du vill radera fingeravtryck för alla kända användare? cancel = Avbryt page-right-thumb = Höger tumme @@ -20,6 +25,7 @@ page-left-index-finger = Vänster pekfinger page-left-middle-finger = Vänster långfinger page-left-ring-finger = Vänster ringfinger page-left-little-finger = Vänster lillfinger +page-delete-all-users-prints = Radera alla användarens fingeravtryck status-connecting = Ansluter till system bus... status-searching-device = Söker efter fingeravtrycksläsare... diff --git a/justfile b/justfile index 13a4adc..ad3cc29 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ name := 'cosmic-ext-fprint' -appid := 'fi.joonastuomi.CosmicFprint' +appid := 'fi.joonastuomi.Fprint' rootdir := '' prefix := '/usr' @@ -61,8 +61,8 @@ run *args: # Installs files install: install -Dm0755 {{bin-src}} {{bin-dst}} - install -Dm0644 resources/app.desktop {{desktop-dst}} - install -Dm0644 resources/app.metainfo.xml {{appdata-dst}} + install -Dm0644 {{desktop-src}} {{desktop-dst}} + install -Dm0644 {{appdata-src}} {{appdata-dst}} install -Dm0644 {{icon-svg-src}} {{icon-svg-dst}} # Uninstalls installed files diff --git a/resources/app.desktop b/resources/fi.joonastuomi.Fprint.desktop similarity index 100% rename from resources/app.desktop rename to resources/fi.joonastuomi.Fprint.desktop diff --git a/resources/app.metainfo.xml b/resources/fi.joonastuomi.Fprint.metainfo.xml similarity index 100% rename from resources/app.metainfo.xml rename to resources/fi.joonastuomi.Fprint.metainfo.xml diff --git a/src/app/fprint.rs b/src/app/fprint.rs index 51c02a7..21777e1 100644 --- a/src/app/fprint.rs +++ b/src/app/fprint.rs @@ -39,6 +39,57 @@ pub async fn delete_fingerprint_dbus( res.and(rel_res) } +pub async fn delete_fingers( + connection: &zbus::Connection, + path: zbus::zvariant::OwnedObjectPath, + username: String, +) -> zbus::Result<()> { + let device = DeviceProxy::builder(connection).path(path)?.build().await?; + + device.claim(&username).await?; + let _ = device.delete_enrolled_fingers2().await; + device.release().await +} + +pub async fn clear_all_fingers_dbus( + connection: &zbus::Connection, + path: zbus::zvariant::OwnedObjectPath, + usernames: Vec, +) -> zbus::Result<()> { + let device = DeviceProxy::builder(connection).path(path)?.build().await?; + let mut last_error = None; + + for username in usernames { + if let Err(e) = device.claim(&username).await { + last_error = Some(e); + continue; + } + + match device.list_enrolled_fingers(&username).await { + Ok(fingers) => { + for finger in fingers { + if let Err(e) = device.delete_enrolled_finger(&finger).await { + last_error = Some(e); + } + } + } + Err(e) => { + last_error = Some(e); + } + } + + if let Err(e) = device.release().await { + last_error = Some(e); + } + } + + if let Some(e) = last_error { + Err(e) + } else { + Ok(()) + } +} + pub async fn enroll_fingerprint_process( connection: zbus::Connection, path: zbus::zvariant::OwnedObjectPath, diff --git a/src/app/message.rs b/src/app/message.rs index e9095b5..d65f3b2 100644 --- a/src/app/message.rs +++ b/src/app/message.rs @@ -22,6 +22,9 @@ pub enum Message { EnrollStatus(String, bool), EnrollStop, DeleteComplete, + ClearDevice, + CancelClear, + ClearComplete(Result<(), AppError>), EnrolledFingers(Vec), UsersFound(Vec), UserSelected(UserOption), diff --git a/src/app/mod.rs b/src/app/mod.rs index e0ae5fa..9ef3072 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -10,7 +10,7 @@ use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::widget::pick_list; use cosmic::iced::{Alignment, Length, Subscription}; use cosmic::prelude::*; -use cosmic::widget::{self, icon, menu, nav_bar, text}; +use cosmic::widget::{self, icon, menu, nav_bar, text, dialog}; use cosmic::{cosmic_theme, theme}; use futures_util::stream::{self, StreamExt}; use futures_util::SinkExt; @@ -26,7 +26,8 @@ pub mod error; use page::{ContextPage, Page}; use message::{Message, UserOption}; use fprint::{ - delete_fingerprint_dbus, enroll_fingerprint_process, find_device, list_enrolled_fingers_dbus, + delete_fingerprint_dbus, delete_fingers, enroll_fingerprint_process, find_device, + clear_all_fingers_dbus,list_enrolled_fingers_dbus, }; use error::AppError; @@ -75,6 +76,8 @@ pub struct AppModel { selected_user: Option, // List of enrolled fingers enrolled_fingers: Vec, + // Confirmation state for clearing the device + confirm_clear: bool, } /// Create a COSMIC application from the app model @@ -150,6 +153,7 @@ impl cosmic::Application for AppModel { realname: Arc::new(u.gecos.to_string_lossy().into_owned()), }), enrolled_fingers: Vec::new(), + confirm_clear: false, }; // Create a startup command that sets the window title. @@ -202,6 +206,27 @@ impl cosmic::Application for AppModel { }) } + /// Display a dialog in the center of the application window when `Some`. + fn dialog(&self) -> Option> { + if self.confirm_clear { + Some( + dialog::dialog() + .title(fl!("clear-device")) + .body(fl!("clear-device-confirm")) + .primary_action( + widget::button::destructive(fl!("clear-device")) + .on_press(Message::ClearDevice), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::CancelClear), + ) + .into(), + ) + } else { + None + } + } + /// Describes the interface based on the current state of the application model. /// /// Application events will be processed through the view. Any messages emitted by @@ -339,18 +364,39 @@ impl cosmic::Application for AppModel { Message::DeleteComplete => { self.status = fl!("deleted"); self.busy = false; - self.enrolled_fingers.retain(|f| { - f != self - .nav - .data::(self.nav.active()) - .map(|p| p.as_finger_id()) - .unwrap_or_default() - }); + if let Some(page) = self.nav.data::(self.nav.active()) { + if let Some(finger_id) = page.as_finger_id() { + self.enrolled_fingers.retain(|f| f != finger_id); + } else { + self.enrolled_fingers.clear(); + } + } Task::none() } Message::Delete => self.on_delete(), + Message::ClearDevice => self.on_clear_device(), + + Message::CancelClear => { + self.confirm_clear = false; + Task::none() + } + + Message::ClearComplete(res) => { + match res { + Ok(_) => { + self.status = fl!("device-cleared"); + self.enrolled_fingers.clear(); + } + Err(e) => { + self.status = e.localized_message(); + } + } + self.busy = false; + Task::none() + } + Message::Register => self.on_register(), Message::OpenRepositoryUrl => { @@ -389,6 +435,10 @@ impl cosmic::Application for AppModel { /// Called when a nav item is selected. fn on_nav_select(&mut self, id: nav_bar::Id) -> Task> { + if self.busy { + return Task::none(); + } + self.confirm_clear = false; // Activate the page in the model. self.nav.activate(id); @@ -566,6 +616,10 @@ impl AppModel { } fn on_user_selected(&mut self, user: UserOption) -> Task> { + if self.busy { + return Task::none(); + } + self.confirm_clear = false; self.selected_user = Some(user.clone()); self.enrolled_fingers.clear(); self.list_fingers_task() @@ -650,6 +704,31 @@ impl AppModel { Task::none() } + fn on_clear_device(&mut self) -> Task> { + if !self.confirm_clear { + self.confirm_clear = true; + return Task::none(); + } + + if let (Some(path), Some(conn)) = (self.device_path.clone(), self.connection.clone()) { + self.status = fl!("clearing-device"); + self.busy = true; + self.confirm_clear = false; + let path = (*path).clone(); + let usernames: Vec = self.users.iter().map(|u| (*u.username).clone()).collect(); + return Task::perform( + async move { + match clear_all_fingers_dbus(&conn, path, usernames).await { + Ok(_) => Message::ClearComplete(Ok(())), + Err(e) => Message::ClearComplete(Err(AppError::from(e))), + } + }, + cosmic::Action::App, + ); + } + Task::none() + } + fn on_delete(&mut self) -> Task> { if let Some(page) = self.nav.data::(self.nav.active()) && let (Some(path), Some(conn), Some(user)) = ( @@ -660,29 +739,43 @@ impl AppModel { { self.status = fl!("deleting"); self.busy = true; - let finger_name = page.as_finger_id().to_string(); let path = (*path).clone(); let username = (*user.username).clone(); - return Task::perform( - async move { - match delete_fingerprint_dbus(&conn, path, finger_name, username).await { - Ok(_) => Message::DeleteComplete, - Err(e) => Message::OperationError(AppError::from(e)), - } - }, - cosmic::Action::App, - ); + + if let Some(finger_name) = page.as_finger_id() { + let finger_name = finger_name.to_string(); + return Task::perform( + async move { + match delete_fingerprint_dbus(&conn, path, finger_name, username).await { + Ok(_) => Message::DeleteComplete, + Err(e) => Message::OperationError(AppError::from(e)), + } + }, + cosmic::Action::App, + ); + } else { + return Task::perform( + async move { + match delete_fingers(&conn, path, username).await { + Ok(_) => Message::DeleteComplete, + Err(e) => Message::OperationError(AppError::from(e)), + } + }, + cosmic::Action::App, + ); + } } Task::none() } fn on_register(&mut self) -> Task> { if let Some(page) = self.nav.data::(self.nav.active()) + && let Some(finger_id) = page.as_finger_id() && self.device_path.is_some() && self.selected_user.is_some() { self.busy = true; - self.enrolling_finger = Some(Arc::new(page.as_finger_id().to_string())); + self.enrolling_finger = Some(Arc::new(finger_id.to_string())); self.status = fl!("status-starting-enrollment"); } Task::none() @@ -764,15 +857,18 @@ impl AppModel { !self.busy && self.device_path.is_some() && self.enrolling_finger.is_none(); let current_page = self.nav.data::(self.nav.active()); - let current_finger = current_page.map(|p| p.as_finger_id()); - let is_enrolled = current_finger - .map(|f| self.enrolled_fingers.iter().any(|ef| ef == f)) - .unwrap_or(false); + let current_finger = current_page.and_then(|p| p.as_finger_id()); + let is_enrolled = if let Some(f) = current_finger { + self.enrolled_fingers.iter().any(|ef| ef == f) + } else { + !self.enrolled_fingers.is_empty() + }; let register_btn = widget::button::text(fl!("register")); let delete_btn = widget::button::text(fl!("delete")); + let clear_btn = widget::button::text(fl!("clear-device")); - let register_btn = if buttons_enabled { + let register_btn = if buttons_enabled && current_finger.is_some() { register_btn.on_press(Message::Register) } else { register_btn @@ -784,6 +880,13 @@ impl AppModel { delete_btn }; + let clear_btn = if !self.busy && self.device_path.is_some() && self.enrolling_finger.is_none() + { + clear_btn.on_press(Message::ClearDevice) + } else { + clear_btn + }; + let mut cancel_btn = widget::button::text(fl!("cancel")); if self.enrolling_finger.is_some() { cancel_btn = cancel_btn.on_press(Message::EnrollStop); @@ -791,7 +894,8 @@ impl AppModel { let mut row = widget::row() .push(register_btn) - .push(delete_btn); + .push(delete_btn) + .push(clear_btn); if self.enrolling_finger.is_some() { row = row.push(cancel_btn); diff --git a/src/app/page.rs b/src/app/page.rs index 5103f64..c8782e0 100644 --- a/src/app/page.rs +++ b/src/app/page.rs @@ -16,6 +16,7 @@ pub enum Page { LeftMiddle, LeftRing, LeftPinky, + DeleteAllUsersPrints, } impl Page { @@ -31,6 +32,7 @@ impl Page { Self::LeftMiddle, Self::LeftRing, Self::LeftPinky, + Self::DeleteAllUsersPrints, ] } @@ -46,21 +48,23 @@ impl Page { Self::LeftMiddle => fl!("page-left-middle-finger"), Self::LeftRing => fl!("page-left-ring-finger"), Self::LeftPinky => fl!("page-left-little-finger"), + Self::DeleteAllUsersPrints => fl!("page-delete-all-users-prints"), } } - pub fn as_finger_id(&self) -> &'static str { + pub fn as_finger_id(&self) -> Option<&'static str> { match self { - Page::RightThumb => "right-thumb", - Page::RightIndex => "right-index-finger", - Page::RightMiddle => "right-middle-finger", - Page::RightRing => "right-ring-finger", - Page::RightPinky => "right-little-finger", - Page::LeftThumb => "left-thumb", - Page::LeftIndex => "left-index-finger", - Page::LeftMiddle => "left-middle-finger", - Page::LeftRing => "left-ring-finger", - Page::LeftPinky => "left-little-finger", + Page::RightThumb => Some("right-thumb"), + Page::RightIndex => Some("right-index-finger"), + Page::RightMiddle => Some("right-middle-finger"), + Page::RightRing => Some("right-ring-finger"), + Page::RightPinky => Some("right-little-finger"), + Page::LeftThumb => Some("left-thumb"), + Page::LeftIndex => Some("left-index-finger"), + Page::LeftMiddle => Some("left-middle-finger"), + Page::LeftRing => Some("left-ring-finger"), + Page::LeftPinky => Some("left-little-finger"), + Page::DeleteAllUsersPrints => None, } } } @@ -79,7 +83,7 @@ mod tests { #[test] fn test_page_all() { let pages = Page::all(); - assert_eq!(pages.len(), 10); + assert_eq!(pages.len(), 11); assert_eq!(pages[0], Page::RightThumb); assert_eq!(pages[1], Page::RightIndex); assert_eq!(pages[2], Page::RightMiddle); @@ -90,6 +94,7 @@ mod tests { assert_eq!(pages[7], Page::LeftMiddle); assert_eq!(pages[8], Page::LeftRing); assert_eq!(pages[9], Page::LeftPinky); + assert_eq!(pages[10], Page::DeleteAllUsersPrints); } #[test] @@ -106,19 +111,21 @@ mod tests { assert!(!Page::LeftMiddle.localized_name().is_empty()); assert!(!Page::LeftRing.localized_name().is_empty()); assert!(!Page::LeftPinky.localized_name().is_empty()); + assert!(!Page::DeleteAllUsersPrints.localized_name().is_empty()); } #[test] fn test_page_as_finger_id() { - assert_eq!(Page::RightThumb.as_finger_id(), "right-thumb"); - assert_eq!(Page::RightIndex.as_finger_id(), "right-index-finger"); - assert_eq!(Page::RightMiddle.as_finger_id(), "right-middle-finger"); - assert_eq!(Page::RightRing.as_finger_id(), "right-ring-finger"); - assert_eq!(Page::RightPinky.as_finger_id(), "right-little-finger"); - assert_eq!(Page::LeftThumb.as_finger_id(), "left-thumb"); - assert_eq!(Page::LeftIndex.as_finger_id(), "left-index-finger"); - assert_eq!(Page::LeftMiddle.as_finger_id(), "left-middle-finger"); - assert_eq!(Page::LeftRing.as_finger_id(), "left-ring-finger"); - assert_eq!(Page::LeftPinky.as_finger_id(), "left-little-finger"); + assert_eq!(Page::RightThumb.as_finger_id(), Some("right-thumb")); + assert_eq!(Page::RightIndex.as_finger_id(), Some("right-index-finger")); + assert_eq!(Page::RightMiddle.as_finger_id(), Some("right-middle-finger")); + assert_eq!(Page::RightRing.as_finger_id(), Some("right-ring-finger")); + assert_eq!(Page::RightPinky.as_finger_id(), Some("right-little-finger")); + assert_eq!(Page::LeftThumb.as_finger_id(), Some("left-thumb")); + assert_eq!(Page::LeftIndex.as_finger_id(), Some("left-index-finger")); + assert_eq!(Page::LeftMiddle.as_finger_id(), Some("left-middle-finger")); + assert_eq!(Page::LeftRing.as_finger_id(), Some("left-ring-finger")); + assert_eq!(Page::LeftPinky.as_finger_id(), Some("left-little-finger")); + assert_eq!(Page::DeleteAllUsersPrints.as_finger_id(), None); } } diff --git a/src/fprint_dbus.rs b/src/fprint_dbus.rs index 3cb8f8d..91e671c 100644 --- a/src/fprint_dbus.rs +++ b/src/fprint_dbus.rs @@ -20,6 +20,7 @@ pub trait Device { fn release(&self) -> zbus::Result<()>; fn list_enrolled_fingers(&self, username: &str) -> zbus::Result>; fn delete_enrolled_finger(&self, finger_name: &str) -> zbus::Result<()>; + fn delete_enrolled_fingers2(&self) -> zbus::Result<()>; fn enroll_start(&self, finger_name: &str) -> zbus::Result<()>; fn enroll_stop(&self) -> zbus::Result<()>;