Add simple file properties

This commit is contained in:
Jeremy Soller 2024-01-05 14:44:20 -07:00
parent bb5403faf9
commit 42c4ec9dad
No known key found for this signature in database
GPG key ID: DCFCA852D3906975
6 changed files with 180 additions and 30 deletions

51
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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<Message> {
let mut children = Vec::new();
let entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data::<Tab>(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<Message> {
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(),
})
}

View file

@ -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

View file

@ -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<Item> {
};
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<Item> {
}
};
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<Item> {
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<Item> {
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<Instant>,
}
impl Item {
pub fn property_view(&self) -> Element<crate::Message> {
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::<chrono::Local>::from(time).format("%c")
))
.into(),
);
}
if let Ok(time) = self.metadata.modified() {
children.push(
widget::text(format!(
"Modified: {}",
chrono::DateTime::<chrono::Local>::from(time).format("%c")
))
.into(),
);
}
if let Ok(time) = self.metadata.created() {
children.push(
widget::text(format!(
"Created: {}",
chrono::DateTime::<chrono::Local>::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);