diff --git a/Cargo.lock b/Cargo.lock index d8d6fc9..39f3b64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -705,6 +711,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "pure-rust-locales", + "wasm-bindgen", + "windows-targets 0.48.5", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -945,6 +966,7 @@ dependencies = [ name = "cosmic-files" version = "0.1.0" dependencies = [ + "chrono", "dirs 5.0.1", "env_logger", "fork", @@ -2270,6 +2292,29 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.6", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "iced" version = "0.12.0" @@ -3930,6 +3975,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135ede8821cf6376eb7a64148901e1690b788c11ae94dc297ae917dbc91dc0e" +[[package]] +name = "pure-rust-locales" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed02a829e62dc2715ceb8afb4f80e298148e1345749ceb369540fe0eb3368432" + [[package]] name = "qoi" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index e98b402..2a9689b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = { version = "0.4", features = ["unstable-locales"] } dirs = "5.0.1" env_logger = "0.10" lazy_static = "1" diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index fc28756..6e997e7 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -3,6 +3,9 @@ empty-folder-hidden = Empty folder (has hidden items) # Context Pages +## Properties +properties = Properties + ## Settings settings = Settings @@ -19,4 +22,3 @@ new-folder = New folder copy = Copy paste = Paste select-all = Select all -new-tab = New tab diff --git a/src/main.rs b/src/main.rs index 776389f..0da2de5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,6 +103,7 @@ pub enum Action { NewFile, NewFolder, Paste, + Properties, SelectAll, Settings, TabNew, @@ -115,6 +116,7 @@ impl Action { Action::NewFile => Message::NewFile(Some(entity)), Action::NewFolder => Message::NewFolder(Some(entity)), Action::Paste => Message::Paste(Some(entity)), + Action::Properties => Message::ToggleContextPage(ContextPage::Properties), Action::SelectAll => Message::SelectAll(Some(entity)), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Action::TabNew => Message::TabNew, @@ -146,12 +148,14 @@ pub enum Message { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ContextPage { + Properties, Settings, } impl ContextPage { fn title(&self) -> String { match self { + Self::Properties => fl!("properties"), Self::Settings => fl!("settings"), } } @@ -231,6 +235,21 @@ impl App { self.set_window_title(window_title) } + fn properties(&self) -> Element { + let mut children = Vec::new(); + let entity = self.tab_model.active(); + if let Some(tab) = self.tab_model.data::(entity) { + if let Some(ref items) = tab.items_opt { + for item in items.iter() { + if item.select_time.is_some() { + children.push(item.property_view()); + } + } + } + } + widget::settings::view_column(children).into() + } + fn settings(&self) -> Element { let app_theme_selected = match self.config.app_theme { AppTheme::Dark => 1, @@ -447,6 +466,7 @@ impl Application for App { } Some(match self.context_page { + ContextPage::Properties => self.properties(), ContextPage::Settings => self.settings(), }) } diff --git a/src/menu.rs b/src/menu.rs index f3dfa3d..b2c8214 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -41,8 +41,7 @@ pub fn context_menu<'a>(entity: segmented_button::Entity) -> Element<'a, Message menu_action(fl!("paste"), Action::Paste), menu_action(fl!("select-all"), Action::SelectAll), horizontal_rule(1), - menu_action(fl!("new-tab"), Action::TabNew), - menu_action(fl!("settings"), Action::Settings), + menu_action(fl!("properties"), Action::Properties), )) .padding(1) //TODO: move style to libcosmic diff --git a/src/tab.rs b/src/tab.rs index 8ea2270..38043be 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -10,7 +10,8 @@ use cosmic::{ use std::{ cmp::Ordering, collections::HashMap, - fmt, fs, + fmt, + fs::{self, Metadata}, path::PathBuf, process, time::{Duration, Instant}, @@ -92,24 +93,16 @@ fn folder_icon(path: &PathBuf, icon_size: u16) -> widget::icon::Handle { } #[cfg(not(target_os = "windows"))] -fn hidden_attribute(_path: &PathBuf) -> bool { +fn hidden_attribute(_metadata: &Metadata) -> bool { false } #[cfg(target_os = "windows")] -fn hidden_attribute(path: &PathBuf) -> bool { +fn hidden_attribute(metadata: &Metadata) -> bool { use std::os::windows::fs::MetadataExt; - match fs::metadata(path) { - Ok(metadata) => { - // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants - const FILE_ATTRIBUTE_HIDDEN: u32 = 2; - metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN - } - Err(err) => { - log::warn!("failed to get hidden attribute for {:?}: {}", path, err); - false - } - } + // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + const FILE_ATTRIBUTE_HIDDEN: u32 = 2; + metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN == FILE_ATTRIBUTE_HIDDEN } #[cfg(target_os = "linux")] @@ -156,7 +149,7 @@ pub fn rescan(tab_path: PathBuf) -> Vec { }; let name = match entry.file_name().into_string() { - Ok(some) => some, + Ok(ok) => ok, Err(name_os) => { log::warn!( "failed to parse entry in {:?}: {:?} is not valid UTF-8", @@ -167,11 +160,24 @@ pub fn rescan(tab_path: PathBuf) -> Vec { } }; + let metadata = match entry.metadata() { + Ok(ok) => ok, + Err(err) => { + log::warn!( + "failed to read metadata for entry in {:?}: {}", + tab_path, + err + ); + continue; + } + }; + + let hidden = name.starts_with(".") || hidden_attribute(&metadata); + let path = entry.path(); - let hidden = name.starts_with(".") || hidden_attribute(&path); - let is_dir = path.is_dir(); + //TODO: configurable size - let (icon_handle_grid, icon_handle_list) = if is_dir { + let (icon_handle_grid, icon_handle_list) = if metadata.is_dir() { ( folder_icon(&path, ICON_SIZE_GRID), folder_icon(&path, ICON_SIZE_LIST), @@ -185,9 +191,9 @@ pub fn rescan(tab_path: PathBuf) -> Vec { items.push(Item { name, - path, + metadata, hidden, - is_dir, + path, icon_handle_grid, icon_handle_list, select_time: None, @@ -198,7 +204,7 @@ pub fn rescan(tab_path: PathBuf) -> Vec { log::warn!("failed to read directory {:?}: {}", tab_path, err); } } - items.sort_by(|a, b| match (a.is_dir, b.is_dir) { + items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) { (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => lexical_sort::natural_lexical_cmp(&a.name, &b.name), @@ -216,22 +222,93 @@ pub enum Message { #[derive(Clone)] pub struct Item { pub name: String, - pub path: PathBuf, + pub metadata: Metadata, pub hidden: bool, - pub is_dir: bool, + pub path: PathBuf, pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub select_time: Option, } +impl Item { + pub fn property_view(&self) -> Element { + let mut children = Vec::new(); + children.push( + widget::icon::icon(self.icon_handle_grid.clone()) + .size(ICON_SIZE_GRID) + .into(), + ); + children.push(widget::text(self.name.clone()).into()); + + //TODO: translate! + { + const KIB: u64 = 1024; + const MIB: u64 = 1024 * KIB; + const GIB: u64 = 1024 * MIB; + const TIB: u64 = 1024 * GIB; + + fn format_size(size: u64) -> String { + if size >= 4 * TIB { + format!("{:.1} TiB", size as f64 / TIB as f64) + } else if size >= GIB { + format!("{:.1} GiB", size as f64 / GIB as f64) + } else if size >= MIB { + format!("{:.1} MiB", size as f64 / MIB as f64) + } else if size >= KIB { + format!("{:.1} KiB", size as f64 / KIB as f64) + } else { + format!("{} B", size) + } + } + + children + .push(widget::text(format!("Size: {}", format_size(self.metadata.len()))).into()); + } + + if let Ok(time) = self.metadata.accessed() { + children.push( + widget::text(format!( + "Accessed: {}", + chrono::DateTime::::from(time).format("%c") + )) + .into(), + ); + } + + if let Ok(time) = self.metadata.modified() { + children.push( + widget::text(format!( + "Modified: {}", + chrono::DateTime::::from(time).format("%c") + )) + .into(), + ); + } + + if let Ok(time) = self.metadata.created() { + children.push( + widget::text(format!( + "Created: {}", + chrono::DateTime::::from(time).format("%c") + )) + .into(), + ); + } + + widget::column::with_children(children) + .width(Length::Fill) + .into() + } +} + impl fmt::Debug for Item { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Item") .field("name", &self.name) - .field("path", &self.path) + .field("metadata", &self.metadata) .field("hidden", &self.hidden) - .field("is_dir", &self.is_dir) - //icon_handles + .field("path", &self.path) + // icon_handles .field("select_time", &self.select_time) .finish() } @@ -282,7 +359,7 @@ impl Tab { if Some(i) == click_i_opt { if let Some(select_time) = item.select_time { if select_time.elapsed() < DOUBLE_CLICK_DURATION { - if item.is_dir { + if item.metadata.is_dir() { cd = Some(item.path.clone()); } else { let mut command = open_command(&item.path);