diff --git a/appa-cosmic/src/app.rs b/appa-cosmic/src/app.rs index b91fa32..1b375e7 100644 --- a/appa-cosmic/src/app.rs +++ b/appa-cosmic/src/app.rs @@ -1,15 +1,13 @@ -use std::{path::PathBuf, sync::LazyLock}; +use std::{borrow::Cow, cmp::Ordering, fmt, path::PathBuf}; use appa::core::{Dashboard, FolderDetails, FolderSummary}; use cosmic::{ iced::{Alignment, Length}, prelude::*, - widget::{self, menu}, + widget::{self, table}, }; const APP_ID: &str = "io.github.appa.Appa"; -static MENU_ID: LazyLock = - LazyLock::new(|| cosmic::iced::id::Id::new("appa-menu")); pub struct AppModel { core: cosmic::Core, @@ -21,16 +19,18 @@ pub struct AppModel { pending_revocation: Option<(PathBuf, String)>, notice: Option, active_form: Option, + folder_table: table::SingleSelectModel, } #[derive(Clone, Debug)] pub enum Message { - Surface(cosmic::surface::Action), DashboardLoaded(Result), FolderDetailsLoaded(Result), FolderPathChanged(String), InvitationChanged(String), Refresh, + FolderSelected(table::Entity), + SortFolders(FolderColumn), ShowAddFolder, ShowJoinFolder, CloseForm, @@ -38,7 +38,6 @@ pub enum Message { JoinFolder, CreateInvite(PathBuf), InvitationCreated(Result), - ShowFolder(PathBuf), CloseFolderDetails, OpenFolder(PathBuf), RequestRevocation { @@ -55,25 +54,79 @@ enum FolderForm { Join, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum MenuAction { - AddFolder, - JoinFolder, - Refresh, +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum FolderColumn { + #[default] + Name, + Synced, + Devices, + Files, } -impl menu::Action for MenuAction { - type Message = Message; +impl fmt::Display for FolderColumn { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::Name => "Name", + Self::Synced => "Synced", + Self::Devices => "Devices", + Self::Files => "Files", + }) + } +} - fn message(&self) -> Self::Message { +impl table::ItemCategory for FolderColumn { + fn width(&self) -> Length { match self { - Self::AddFolder => Message::ShowAddFolder, - Self::JoinFolder => Message::ShowJoinFolder, - Self::Refresh => Message::Refresh, + Self::Name => Length::Fill, + Self::Synced => Length::Fixed(200.0), + Self::Devices => Length::Fixed(140.0), + Self::Files => Length::Fixed(100.0), } } } +#[derive(Clone, Debug)] +struct FolderTableItem { + path: PathBuf, + name: String, + synced: String, + devices: String, + files: String, +} + +impl FolderTableItem { + fn from_summary(folder: &FolderSummary) -> Self { + Self { + path: folder.path.clone(), + name: folder.name.clone(), + synced: folder_health(folder), + devices: format!("{} devices", folder.member_count), + files: format!("{} files", folder.file_count), + } + } +} + +impl table::ItemInterface for FolderTableItem { + fn get_icon(&self, category: FolderColumn) -> Option { + (category == FolderColumn::Name).then(|| widget::icon::from_name("folder-symbolic").icon()) + } + + fn get_text(&self, category: FolderColumn) -> Cow<'static, str> { + match category { + FolderColumn::Name => self.name.clone().into(), + FolderColumn::Synced => self.synced.clone().into(), + FolderColumn::Devices => self.devices.clone().into(), + FolderColumn::Files => self.files.clone().into(), + } + } + + fn compare(&self, other: &Self, category: FolderColumn) -> Ordering { + self.get_text(category) + .to_lowercase() + .cmp(&other.get_text(category).to_lowercase()) + } +} + impl cosmic::Application for AppModel { type Executor = cosmic::executor::Default; type Flags = (); @@ -100,6 +153,12 @@ impl cosmic::Application for AppModel { pending_revocation: None, notice: None, active_form: None, + folder_table: table::Model::new(vec![ + FolderColumn::Name, + FolderColumn::Synced, + FolderColumn::Devices, + FolderColumn::Files, + ]), }; (app, load_dashboard()) } @@ -114,13 +173,9 @@ impl cosmic::Application for AppModel { fn update(&mut self, message: Message) -> Task> { match message { - Message::Surface(action) => { - return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(action), - )); - } Message::DashboardLoaded(result) => match result { Ok(dashboard) => { + update_folder_table(&mut self.folder_table, &dashboard.folders); self.dashboard = Some(dashboard); self.notice = None; } @@ -133,6 +188,14 @@ impl cosmic::Application for AppModel { Message::FolderPathChanged(path) => self.folder_path = path, Message::InvitationChanged(invitation) => self.invitation = invitation, Message::Refresh => return load_dashboard(), + Message::FolderSelected(entity) => { + self.folder_table.activate(entity); + if let Some(folder) = self.folder_table.item(entity) { + self.selected_folder = Some(folder.path.clone()); + return load_folder_details(folder.path.clone()); + } + } + Message::SortFolders(column) => sort_folder_table(&mut self.folder_table, column), Message::ShowAddFolder => self.active_form = Some(FolderForm::Add), Message::ShowJoinFolder => self.active_form = Some(FolderForm::Join), Message::CloseForm => self.active_form = None, @@ -149,10 +212,6 @@ impl cosmic::Application for AppModel { } Err(error) => self.notice = Some(error), }, - Message::ShowFolder(path) => { - self.selected_folder = Some(path.clone()); - return load_folder_details(path); - } Message::CloseFolderDetails => { self.details = None; self.selected_folder = None; @@ -178,23 +237,18 @@ impl cosmic::Application for AppModel { Task::none() } - fn header_start(&self) -> Vec> { - let key_bindings = std::collections::HashMap::new(); - vec![widget::responsive_menu_bar().into_element( - self.core(), - &key_bindings, - MENU_ID.clone(), - Message::Surface, - vec![( - "Folders", - vec![ - menu::Item::Button("Add folder", None, MenuAction::AddFolder), - menu::Item::Button("Join folder", None, MenuAction::JoinFolder), - menu::Item::Divider, - menu::Item::Button("Refresh", None, MenuAction::Refresh), - ], - )], - )] + fn header_end(&self) -> Vec> { + vec![ + widget::button::text("Add") + .on_press(Message::ShowAddFolder) + .into(), + widget::button::text("Join") + .on_press(Message::ShowJoinFolder) + .into(), + widget::button::text("Refresh") + .on_press(Message::Refresh) + .into(), + ] } } @@ -211,13 +265,19 @@ impl AppModel { if let Some(form) = self.active_form { content = content.push(self.folder_form(form)); } - if let Some(dashboard) = &self.dashboard { - if dashboard.folders.is_empty() { + if self.dashboard.is_some() { + if self.folder_table.iter().next().is_none() { content = content.push(widget::text::body("No folders are being synchronized yet.")); + } else { + content = content.push( + widget::table(&self.folder_table) + .on_item_left_click(Message::FolderSelected) + .on_category_left_click(Message::SortFolders), + ); } - for folder in &dashboard.folders { - content = content.push(self.folder_row(folder)); + if let Some(details) = &self.details { + content = content.push(self.folder_details_view(details)); } } else { content = content.push(widget::text::body( @@ -338,35 +398,6 @@ impl AppModel { } content.into() } - - fn folder_row<'a>(&'a self, folder: &'a FolderSummary) -> Element<'a, Message> { - let health = folder_health(folder); - let metadata = format!( - "{} files · {} devices · {} connected peers · {health}", - folder.file_count, folder.member_count, folder.peer_count - ); - let mut row = widget::column::with_capacity(2).push( - widget::row::with_capacity(2) - .push( - widget::column::with_capacity(3) - .push(widget::text::title3(&folder.name)) - .push(widget::text::body(folder.path.display().to_string())) - .push(widget::text::caption(metadata)), - ) - .push( - widget::button::text("More").on_press(Message::ShowFolder(folder.path.clone())), - ) - .align_y(Alignment::Center) - .spacing(cosmic::theme::spacing().space_s), - ); - - if self.selected_folder.as_ref() == Some(&folder.path) { - if let Some(details) = &self.details { - row = row.push(self.folder_details_view(details)); - } - } - row.spacing(cosmic::theme::spacing().space_s).into() - } } fn folder_health(folder: &FolderSummary) -> String { @@ -382,6 +413,26 @@ fn folder_health(folder: &FolderSummary) -> String { } } +fn update_folder_table( + table: &mut table::SingleSelectModel, + folders: &[FolderSummary], +) { + table.clear(); + for folder in folders { + let _ = table.insert(FolderTableItem::from_summary(folder)); + } +} + +fn sort_folder_table( + table: &mut table::SingleSelectModel, + column: FolderColumn, +) { + let ascending = table + .get_sort() + .is_none_or(|(current_column, ascending)| current_column != column || !ascending); + table.sort(column, ascending); +} + fn load_dashboard() -> Task> { Task::perform( async {