perf: general minor performance optimisations
Notably there is some code cleanup with the zooming functionality, I've created a new module to reduce code duplication.
This commit is contained in:
parent
5f729829d7
commit
bd1fa1f0a9
16 changed files with 971 additions and 1109 deletions
716
src/app.rs
716
src/app.rs
File diff suppressed because it is too large
Load diff
|
|
@ -47,7 +47,7 @@ pub fn extract(
|
||||||
controller: &Controller,
|
controller: &Controller,
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
let mime = mime_for_path(path, None, false);
|
let mime = mime_for_path(path, None, false);
|
||||||
let password = password.clone();
|
let password = password.as_deref();
|
||||||
match mime.essence_str() {
|
match mime.essence_str() {
|
||||||
"application/gzip" | "application/x-compressed-tar" => {
|
"application/gzip" | "application/x-compressed-tar" => {
|
||||||
OpReader::new(path, controller.clone())
|
OpReader::new(path, controller.clone())
|
||||||
|
|
@ -107,7 +107,7 @@ pub fn extract(
|
||||||
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
archive: &mut zip::ZipArchive<R>,
|
archive: &mut zip::ZipArchive<R>,
|
||||||
directory: P,
|
directory: P,
|
||||||
password: Option<String>,
|
password: Option<&str>,
|
||||||
controller: Controller,
|
controller: Controller,
|
||||||
) -> zip::result::ZipResult<()> {
|
) -> zip::result::ZipResult<()> {
|
||||||
use std::{ffi::OsString, fs};
|
use std::{ffi::OsString, fs};
|
||||||
|
|
@ -145,7 +145,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
|
|
||||||
controller.set_progress((i as f32) / total_files as f32);
|
controller.set_progress((i as f32) / total_files as f32);
|
||||||
|
|
||||||
let mut file = match &password {
|
let mut file = match password {
|
||||||
None => archive.by_index(i),
|
None => archive.by_index(i),
|
||||||
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
||||||
}?;
|
}?;
|
||||||
|
|
@ -207,7 +207,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut file = match &password {
|
let mut file = match password {
|
||||||
None => archive.by_index(i),
|
None => archive.by_index(i),
|
||||||
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
|
||||||
}?;
|
}?;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub struct ClipboardCopy {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClipboardCopy {
|
impl ClipboardCopy {
|
||||||
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self {
|
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: impl IntoIterator<Item = P>) -> Self {
|
||||||
let available = vec![
|
let available = vec![
|
||||||
"text/plain".to_string(),
|
"text/plain".to_string(),
|
||||||
"text/plain;charset=utf-8".to_string(),
|
"text/plain;charset=utf-8".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -75,21 +75,17 @@ pub enum Favorite {
|
||||||
impl Favorite {
|
impl Favorite {
|
||||||
pub fn from_path(path: PathBuf) -> Self {
|
pub fn from_path(path: PathBuf) -> Self {
|
||||||
// Ensure that special folders are handled properly
|
// Ensure that special folders are handled properly
|
||||||
for favorite in &[
|
[
|
||||||
Self::Home,
|
Self::Home,
|
||||||
Self::Documents,
|
Self::Documents,
|
||||||
Self::Downloads,
|
Self::Downloads,
|
||||||
Self::Music,
|
Self::Music,
|
||||||
Self::Pictures,
|
Self::Pictures,
|
||||||
Self::Videos,
|
Self::Videos,
|
||||||
] {
|
]
|
||||||
if let Some(favorite_path) = favorite.path_opt() {
|
.into_iter()
|
||||||
if favorite_path == path {
|
.find(|fav| fav.path_opt().as_ref() == Some(&path))
|
||||||
return favorite.clone();
|
.unwrap_or(Self::Path(path))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Path(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_opt(&self) -> Option<PathBuf> {
|
pub fn path_opt(&self) -> Option<PathBuf> {
|
||||||
|
|
|
||||||
122
src/dialog.rs
122
src/dialog.rs
|
|
@ -31,9 +31,7 @@ use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
env, fmt, fs,
|
env, fmt, fs,
|
||||||
num::NonZeroU16,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
|
||||||
time::{self, Instant},
|
time::{self, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,6 +46,7 @@ use crate::{
|
||||||
menu,
|
menu,
|
||||||
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
||||||
tab::{self, ItemMetadata, Location, Tab},
|
tab::{self, ItemMetadata, Location, Tab},
|
||||||
|
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -372,7 +371,7 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
let on_result_message = (self.on_result)(result);
|
let on_result_message = (self.on_result)(result);
|
||||||
Task::batch([
|
Task::batch([
|
||||||
command,
|
command,
|
||||||
Task::perform(async move { cosmic::action::app(on_result_message) }, |x| x),
|
Task::future(async move { cosmic::action::app(on_result_message) }),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
command
|
command
|
||||||
|
|
@ -606,7 +605,7 @@ impl App {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
//TODO: easier way to create buttons with rich text
|
//TODO: easier way to create buttons with rich text
|
||||||
widget::button::custom(
|
widget::button::custom(
|
||||||
widget::row::with_children(vec![Element::from(&self.accept_label)])
|
widget::row::with_children([Element::from(&self.accept_label)])
|
||||||
.padding([0, space_s])
|
.padding([0, space_s])
|
||||||
.width(Length::Shrink)
|
.width(Length::Shrink)
|
||||||
.height(space_l)
|
.height(space_l)
|
||||||
|
|
@ -677,16 +676,15 @@ impl App {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
let mounter_items = self.mounter_items.clone();
|
let mounter_items = self.mounter_items.clone();
|
||||||
Task::perform(
|
Task::future(async move {
|
||||||
async move {
|
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||||
Ok((parent_item_opt, mut items)) => {
|
Ok((parent_item_opt, mut items)) => {
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
{
|
{
|
||||||
let mounter_paths: Vec<_> = mounter_items
|
let mounter_paths: Box<[_]> = mounter_items
|
||||||
.iter()
|
.values()
|
||||||
.flat_map(|item| item.1.iter())
|
.flatten()
|
||||||
.filter_map(MounterItem::path)
|
.filter_map(MounterItem::path)
|
||||||
.collect();
|
.collect();
|
||||||
if !mounter_paths.is_empty() {
|
if !mounter_paths.is_empty() {
|
||||||
|
|
@ -708,9 +706,7 @@ impl App {
|
||||||
cosmic::action::none()
|
cosmic::action::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
|x| x,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_get(&self) -> Option<&str> {
|
fn search_get(&self) -> Option<&str> {
|
||||||
|
|
@ -835,12 +831,10 @@ impl App {
|
||||||
// Collect all mounter items
|
// Collect all mounter items
|
||||||
let mut nav_items = Vec::new();
|
let mut nav_items = Vec::new();
|
||||||
for (key, items) in &self.mounter_items {
|
for (key, items) in &self.mounter_items {
|
||||||
for item in items {
|
nav_items.extend(items.iter().map(|item| (*key, item)));
|
||||||
nav_items.push((*key, item));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Sort by name lexically
|
// Sort by name lexically
|
||||||
nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
|
nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
|
||||||
// Add items to nav model
|
// Add items to nav model
|
||||||
for (i, (key, item)) in nav_items.into_iter().enumerate() {
|
for (i, (key, item)) in nav_items.into_iter().enumerate() {
|
||||||
nav_model = nav_model.insert(|mut b| {
|
nav_model = nav_model.insert(|mut b| {
|
||||||
|
|
@ -1040,7 +1034,7 @@ impl Application for App {
|
||||||
//TODO: should gallery view just be a dialog?
|
//TODO: should gallery view just be a dialog?
|
||||||
if self.tab.gallery {
|
if self.tab.gallery {
|
||||||
return Some(
|
return Some(
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
self.tab.gallery_view().map(Message::TabMessage),
|
self.tab.gallery_view().map(Message::TabMessage),
|
||||||
// Draw button row as part of the overlay
|
// Draw button row as part of the overlay
|
||||||
widget::container(self.button_view())
|
widget::container(self.button_view())
|
||||||
|
|
@ -1098,7 +1092,7 @@ impl Application for App {
|
||||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||||
)
|
)
|
||||||
.control(
|
.control(
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(fl!("folder-name")).into(),
|
widget::text::body(fl!("folder-name")).into(),
|
||||||
widget::text_input("", name.as_str())
|
widget::text_input("", name.as_str())
|
||||||
.id(self.dialog_text_input.clone())
|
.id(self.dialog_text_input.clone())
|
||||||
|
|
@ -1392,10 +1386,9 @@ impl Application for App {
|
||||||
return self.search_set(Some(term));
|
return self.search_set(Some(term));
|
||||||
}
|
}
|
||||||
TypeToSearch::EnterPath => {
|
TypeToSearch::EnterPath => {
|
||||||
let location = self.tab.edit_location.as_ref().map_or_else(
|
let location = (self.tab.edit_location)
|
||||||
|| self.tab.location.clone(),
|
.as_ref()
|
||||||
|x| x.location.clone(),
|
.map_or_else(|| &self.tab.location, |x| &x.location);
|
||||||
);
|
|
||||||
// Try to add text to end of location
|
// Try to add text to end of location
|
||||||
if let Some(path) = location.path_opt() {
|
if let Some(path) = location.path_opt() {
|
||||||
let mut path_string = path.to_string_lossy().to_string();
|
let mut path_string = path.to_string_lossy().to_string();
|
||||||
|
|
@ -1538,7 +1531,7 @@ impl Application for App {
|
||||||
if let Some(path) = item.path_opt() {
|
if let Some(path) = item.path_opt() {
|
||||||
paths.push(path.clone());
|
paths.push(path.clone());
|
||||||
let _ = update_recently_used(
|
let _ = update_recently_used(
|
||||||
&path.clone(),
|
path,
|
||||||
Self::APP_ID.to_string(),
|
Self::APP_ID.to_string(),
|
||||||
"cosmic-files".to_string(),
|
"cosmic-files".to_string(),
|
||||||
None,
|
None,
|
||||||
|
|
@ -1775,9 +1768,9 @@ impl Application for App {
|
||||||
// Filter
|
// Filter
|
||||||
if let Some(filter_i) = self.filter_selected {
|
if let Some(filter_i) = self.filter_selected {
|
||||||
if let Some(filter) = self.filters.get(filter_i) {
|
if let Some(filter) = self.filters.get(filter_i) {
|
||||||
// Parse filters
|
// Parse globs (Mime implements PartialEq with &str, so no need to parse)
|
||||||
let mut parsed_globs = Vec::new();
|
let mut parsed_globs = Vec::new();
|
||||||
let mut parsed_mimes = Vec::new();
|
let mut mimes = Vec::new();
|
||||||
for pattern in &filter.patterns {
|
for pattern in &filter.patterns {
|
||||||
match pattern {
|
match pattern {
|
||||||
DialogFilterPattern::Glob(value) => {
|
DialogFilterPattern::Glob(value) => {
|
||||||
|
|
@ -1788,39 +1781,17 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DialogFilterPattern::Mime(value) => {
|
DialogFilterPattern::Mime(value) => mimes.push(value.as_str()),
|
||||||
match mime_guess::Mime::from_str(value) {
|
|
||||||
Ok(mime) => parsed_mimes.push(mime),
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to parse mime {value:?}: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.retain(|item| {
|
items.retain(|item| {
|
||||||
if item.metadata.is_dir() {
|
|
||||||
// Directories are always shown
|
// Directories are always shown
|
||||||
return true;
|
item.metadata.is_dir()
|
||||||
}
|
|
||||||
|
|
||||||
// Check for mime type match (first because it is faster)
|
// Check for mime type match (first because it is faster)
|
||||||
for mime in &parsed_mimes {
|
|| mimes.iter().copied().any(|mime| mime == item.mime)
|
||||||
if mime == &item.mime {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for glob match (last because it is slower)
|
// Check for glob match (last because it is slower)
|
||||||
for glob in &parsed_globs {
|
|| parsed_globs.iter().any(|glob| glob.matches(&item.name))
|
||||||
if glob.matches(&item.name) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No filters matched
|
|
||||||
false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1869,47 +1840,18 @@ impl Application for App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomDefault => {
|
Message::ZoomDefault => {
|
||||||
return self.with_dialog_config(|config| match config.view {
|
return self.with_dialog_config(|config| {
|
||||||
tab::View::List => config.icon_sizes.list = 100.try_into().unwrap(),
|
zoom_to_default(config.view, &mut config.icon_sizes);
|
||||||
tab::View::Grid => config.icon_sizes.grid = 100.try_into().unwrap(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomIn => {
|
Message::ZoomIn => {
|
||||||
let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| {
|
return self.with_dialog_config(|config| {
|
||||||
let mut step = min;
|
zoom_in_view(config.view, &mut config.icon_sizes);
|
||||||
while step <= max {
|
|
||||||
if size.get() < step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
step += 25;
|
|
||||||
}
|
|
||||||
if size.get() > step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return self.with_dialog_config(|config| match config.view {
|
|
||||||
tab::View::List => zoom_in(&mut config.icon_sizes.list, 50, 500),
|
|
||||||
tab::View::Grid => zoom_in(&mut config.icon_sizes.grid, 50, 500),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::ZoomOut => {
|
Message::ZoomOut => {
|
||||||
let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| {
|
return self.with_dialog_config(|config| {
|
||||||
let mut step = max;
|
zoom_out_view(config.view, &mut config.icon_sizes);
|
||||||
while step >= min {
|
|
||||||
if size.get() > step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
step -= 25;
|
|
||||||
}
|
|
||||||
if size.get() < step {
|
|
||||||
*size = step.try_into().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return self.with_dialog_config(|config| match config.view {
|
|
||||||
tab::View::List => zoom_out(&mut config.icon_sizes.list, 50, 500),
|
|
||||||
tab::View::Grid => zoom_out(&mut config.icon_sizes.grid, 50, 500),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message::Surface(action) => {
|
Message::Surface(action) => {
|
||||||
|
|
@ -2090,8 +2032,7 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, mounter) in MOUNTERS.iter() {
|
subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {
|
||||||
subscriptions.push(
|
|
||||||
mounter
|
mounter
|
||||||
.subscription()
|
.subscription()
|
||||||
.with(*key)
|
.with(*key)
|
||||||
|
|
@ -2102,9 +2043,8 @@ impl Application for App {
|
||||||
log::warn!("{mounter_message:?} not supported in dialog mode");
|
log::warn!("{mounter_message:?} not supported in dialog mode");
|
||||||
Message::None
|
Message::None
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
);
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
Subscription::batch(subscriptions)
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ mod mouse_area;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
mod spawn_detached;
|
mod spawn_detached;
|
||||||
use tab::Location;
|
use tab::Location;
|
||||||
|
mod zoom;
|
||||||
|
|
||||||
use crate::config::State;
|
use crate::config::State;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|
|
||||||
17
src/menu.rs
17
src/menu.rs
|
|
@ -32,7 +32,7 @@ macro_rules! menu_button {
|
||||||
($($x:expr),+ $(,)?) => (
|
($($x:expr),+ $(,)?) => (
|
||||||
button::custom(
|
button::custom(
|
||||||
Row::with_children(
|
Row::with_children(
|
||||||
vec![$(Element::from($x)),+]
|
[$(Element::from($x)),+]
|
||||||
)
|
)
|
||||||
.height(Length::Fixed(24.0))
|
.height(Length::Fixed(24.0))
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
|
|
@ -167,9 +167,9 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
{
|
{
|
||||||
for (i, action) in entry.desktop_actions.into_iter().enumerate() {
|
children.extend(entry.desktop_actions.into_iter().enumerate().map(
|
||||||
children.push(menu_item(action.name, Action::ExecEntryAction(i)).into());
|
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(),
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||||
|
|
@ -207,11 +207,8 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
children.push(menu_item(fl!("copy"), Action::Copy).into());
|
||||||
|
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES
|
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
|
||||||
.iter()
|
selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
|
||||||
.filter_map(|mime_type| mime_type.parse::<Mime>().ok())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
selected_types.retain(|t| !supported_archive_types.contains(t));
|
|
||||||
if selected_types.is_empty() {
|
if selected_types.is_empty() {
|
||||||
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into());
|
||||||
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
|
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into());
|
||||||
|
|
@ -719,7 +716,7 @@ pub fn menu_bar<'a>(
|
||||||
|
|
||||||
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
|
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> {
|
||||||
//TODO: only add some of these when in App mode
|
//TODO: only add some of these when in App mode
|
||||||
let children = vec![
|
let children = [
|
||||||
menu_button!(text::body(fl!("open-in-new-tab")))
|
menu_button!(text::body(fl!("open-in-new-tab")))
|
||||||
.on_press(tab::Message::LocationMenuAction(
|
.on_press(tab::Message::LocationMenuAction(
|
||||||
LocationMenuAction::OpenInNewTab(ancestor_index),
|
LocationMenuAction::OpenInNewTab(ancestor_index),
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ pub fn exec_to_command(
|
||||||
for invalid in path_opt
|
for invalid in path_opt
|
||||||
.iter()
|
.iter()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.filter(|path| from_file_or_dir(path).is_none())
|
.filter(|&path| from_file_or_dir(path).is_none())
|
||||||
{
|
{
|
||||||
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
|
log::warn!("Desktop file expects a file path instead of a URL: {invalid:?}");
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +221,7 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
|
||||||
pub struct MimeAppCache {
|
pub struct MimeAppCache {
|
||||||
apps: Vec<MimeApp>,
|
apps: Vec<MimeApp>,
|
||||||
cache: FxHashMap<Mime, Vec<MimeApp>>,
|
cache: FxHashMap<Mime, Vec<MimeApp>>,
|
||||||
icons: FxHashMap<Mime, Vec<widget::icon::Handle>>,
|
icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
|
||||||
terminals: Vec<MimeApp>,
|
terminals: Vec<MimeApp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +257,7 @@ impl MimeAppCache {
|
||||||
|
|
||||||
// Load desktop applications by supported mime types
|
// Load desktop applications by supported mime types
|
||||||
//TODO: hashmap for all apps by id?
|
//TODO: hashmap for all apps by id?
|
||||||
let all_apps: Vec<_> = desktop::load_applications(locale, false, None).collect();
|
let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();
|
||||||
for app in &all_apps {
|
for app in &all_apps {
|
||||||
//TODO: just collect apps that can be executed with a file argument?
|
//TODO: just collect apps that can be executed with a file argument?
|
||||||
if !app.mime_types.is_empty() {
|
if !app.mime_types.is_empty() {
|
||||||
|
|
@ -292,21 +292,17 @@ impl MimeAppCache {
|
||||||
let mut mimeapps_paths = Vec::new();
|
let mut mimeapps_paths = Vec::new();
|
||||||
let xdg_dirs = xdg::BaseDirectories::new();
|
let xdg_dirs = xdg::BaseDirectories::new();
|
||||||
|
|
||||||
for path in xdg_dirs.find_data_files("applications/mimeapps.list") {
|
mimeapps_paths.extend(xdg_dirs.find_data_files("applications/mimeapps.list"));
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
|
||||||
for desktop in desktops.iter().rev() {
|
for desktop in desktops.iter().rev() {
|
||||||
for path in xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")) {
|
mimeapps_paths
|
||||||
mimeapps_paths.push(path);
|
.extend(xdg_dirs.find_data_files(format!("applications/{desktop}-mimeapps.list")));
|
||||||
}
|
|
||||||
}
|
|
||||||
for path in xdg_dirs.find_config_files("mimeapps.list") {
|
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mimeapps_paths.extend(xdg_dirs.find_config_files("mimeapps.list"));
|
||||||
|
|
||||||
for desktop in desktops.iter().rev() {
|
for desktop in desktops.iter().rev() {
|
||||||
for path in xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")) {
|
mimeapps_paths.extend(xdg_dirs.find_config_files(format!("{desktop}-mimeapps.list")));
|
||||||
mimeapps_paths.push(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: handle directory specific behavior
|
//TODO: handle directory specific behavior
|
||||||
|
|
@ -334,7 +330,7 @@ impl MimeAppCache {
|
||||||
.or_insert_with(|| Vec::with_capacity(1));
|
.or_insert_with(|| Vec::with_capacity(1));
|
||||||
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
|
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
|
||||||
if let Some(app) =
|
if let Some(app) =
|
||||||
all_apps.iter().find(|x| filename_eq(&x.path, filename))
|
all_apps.iter().find(|&x| filename_eq(&x.path, filename))
|
||||||
{
|
{
|
||||||
apps.push(MimeApp::from(app));
|
apps.push(MimeApp::from(app));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -406,12 +402,12 @@ impl MimeAppCache {
|
||||||
|
|
||||||
// Copy icons to special cache
|
// Copy icons to special cache
|
||||||
//TODO: adjust dropdown API so this is no longer needed
|
//TODO: adjust dropdown API so this is no longer needed
|
||||||
for (mime, apps) in &self.cache {
|
self.icons.extend(self.cache.iter().map(|(mime, apps)| {
|
||||||
self.icons.insert(
|
(
|
||||||
mime.clone(),
|
mime.clone(),
|
||||||
apps.iter().map(|app| app.icon.clone()).collect(),
|
apps.iter().map(|app| app.icon.clone()).collect(),
|
||||||
);
|
)
|
||||||
}
|
}));
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
log::info!("loaded mime app cache in {elapsed:?}");
|
log::info!("loaded mime app cache in {elapsed:?}");
|
||||||
|
|
@ -426,7 +422,7 @@ impl MimeAppCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
|
pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
|
||||||
self.icons.get(key).map_or(&[], Vec::as_slice)
|
self.icons.get(key).map_or(&[], Box::as_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_default_terminal(&self) -> Option<String> {
|
fn get_default_terminal(&self) -> Option<String> {
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,8 @@ impl MimeIconCache {
|
||||||
let icon_name = icon_names.remove(0);
|
let icon_name = icon_names.remove(0);
|
||||||
let mut named = icon::from_name(icon_name).size(key.size);
|
let mut named = icon::from_name(icon_name).size(key.size);
|
||||||
if !icon_names.is_empty() {
|
if !icon_names.is_empty() {
|
||||||
let mut fallback_names = Vec::with_capacity(icon_names.len());
|
let fallback_names =
|
||||||
for fallback_name in icon_names {
|
icon_names.into_iter().map(std::borrow::Cow::from).collect();
|
||||||
fallback_names.push(fallback_name.into());
|
|
||||||
}
|
|
||||||
named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
|
named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
|
||||||
}
|
}
|
||||||
Some(named.handle())
|
Some(named.handle())
|
||||||
|
|
@ -55,8 +53,8 @@ impl MimeIconCache {
|
||||||
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
||||||
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
||||||
|
|
||||||
pub fn mime_for_path<P: AsRef<Path>>(
|
pub fn mime_for_path(
|
||||||
path: P,
|
path: impl AsRef<Path>,
|
||||||
metadata_opt: Option<&fs::Metadata>,
|
metadata_opt: Option<&fs::Metadata>,
|
||||||
remote: bool,
|
remote: bool,
|
||||||
) -> Mime {
|
) -> Mime {
|
||||||
|
|
@ -65,7 +63,7 @@ pub fn mime_for_path<P: AsRef<Path>>(
|
||||||
// Try the shared mime info cache first
|
// Try the shared mime info cache first
|
||||||
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
|
||||||
if remote {
|
if remote {
|
||||||
if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
|
||||||
gb.file_name(file_name);
|
gb.file_name(file_name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -75,11 +73,22 @@ pub fn mime_for_path<P: AsRef<Path>>(
|
||||||
gb.metadata(metadata.clone());
|
gb.metadata(metadata.clone());
|
||||||
}
|
}
|
||||||
let guess = gb.guess();
|
let guess = gb.guess();
|
||||||
if guess.uncertain() {
|
let guessed_mime = guess.mime_type();
|
||||||
|
|
||||||
|
/// Checks if the `Mime` is a special variant returned by `xdg-mime`.
|
||||||
|
/// This includes directories, symlinks and zerosize files, which are returned as uncertain.
|
||||||
|
fn is_special_mime(mime: &Mime) -> bool {
|
||||||
|
*mime == "inode/directory" || *mime == "inode/symlink" || *mime == "application/x-zerosize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// `xdg-mime-rs` sets the guess to uncertain if it returns special mime types.
|
||||||
|
// The guess could also be uncertain on platforms without shared-mime-info.
|
||||||
|
// Try mime_guess, but only if it is not one of the special mime types.
|
||||||
|
if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) {
|
||||||
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
|
// If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
|
||||||
mime_guess::from_path(path).first_or_octet_stream()
|
mime_guess::from_path(path).first_or_octet_stream()
|
||||||
} else {
|
} else {
|
||||||
guess.mime_type().clone()
|
guessed_mime.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,45 +30,48 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
||||||
let mut items = MounterItems::new();
|
let mut items: MounterItems = (monitor.mounts().into_iter())
|
||||||
for (i, mount) in monitor.mounts().into_iter().enumerate() {
|
.enumerate()
|
||||||
items.push(MounterItem::Gvfs(Item {
|
.map(|(i, mount)| {
|
||||||
uri: MountExt::root(&mount).uri().to_string(),
|
MounterItem::Gvfs(Item {
|
||||||
|
uri: mount.root().uri().into(),
|
||||||
kind: ItemKind::Mount,
|
kind: ItemKind::Mount,
|
||||||
index: i,
|
index: i,
|
||||||
name: MountExt::name(&mount).to_string(),
|
name: mount.name().into(),
|
||||||
is_mounted: true,
|
is_mounted: true,
|
||||||
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
|
icon_opt: gio_icon_to_path(&MountExt::icon(&mount), sizes.grid()),
|
||||||
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
|
icon_symbolic_opt: gio_icon_to_path(&MountExt::symbolic_icon(&mount), 16),
|
||||||
path_opt: MountExt::root(&mount).path(),
|
path_opt: MountExt::root(&mount).path(),
|
||||||
}));
|
})
|
||||||
}
|
})
|
||||||
for (i, volume) in monitor.volumes().into_iter().enumerate() {
|
.collect();
|
||||||
if volume.get_mount().is_some() {
|
items.extend(
|
||||||
|
(monitor.volumes().into_iter())
|
||||||
|
.enumerate()
|
||||||
// Volumes with mounts are already listed by mount
|
// Volumes with mounts are already listed by mount
|
||||||
continue;
|
.filter(|(_, volume)| volume.get_mount().is_none())
|
||||||
}
|
.map(|(i, volume)| {
|
||||||
let uri = VolumeExt::activation_root(&volume)
|
let uri = VolumeExt::activation_root(&volume)
|
||||||
.map(|f| f.uri().to_string())
|
.map(|f| f.uri().into())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
items.push(MounterItem::Gvfs(Item {
|
MounterItem::Gvfs(Item {
|
||||||
// TODO can we get URI for volumes with no mount?
|
// TODO can we get URI for volumes with no mount?
|
||||||
uri,
|
uri,
|
||||||
kind: ItemKind::Volume,
|
kind: ItemKind::Volume,
|
||||||
index: i,
|
index: i,
|
||||||
name: VolumeExt::name(&volume).to_string(),
|
name: volume.name().into(),
|
||||||
is_mounted: false,
|
is_mounted: false,
|
||||||
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
|
icon_opt: gio_icon_to_path(&VolumeExt::icon(&volume), sizes.grid()),
|
||||||
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
|
icon_symbolic_opt: gio_icon_to_path(&VolumeExt::symbolic_icon(&volume), 16),
|
||||||
path_opt: None,
|
path_opt: None,
|
||||||
}));
|
})
|
||||||
}
|
}),
|
||||||
|
);
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
let mut uri = uri.to_string();
|
let mut file = gio::File::for_uri(uri);
|
||||||
let mut file = gio::File::for_uri(&uri);
|
|
||||||
let force_dir = uri.starts_with("network:///");
|
let force_dir = uri.starts_with("network:///");
|
||||||
|
|
||||||
// Resolve the target-uri if it exists
|
// Resolve the target-uri if it exists
|
||||||
|
|
@ -78,8 +81,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
) {
|
) {
|
||||||
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
||||||
uri = resolved_uri.to_string();
|
file = gio::File::for_uri(resolved_uri.as_str());
|
||||||
file = gio::File::for_uri(&uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,10 +91,10 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
.map_err(err_str)?
|
.map_err(err_str)?
|
||||||
{
|
{
|
||||||
let info = info_res.map_err(err_str)?;
|
let info = info_res.map_err(err_str)?;
|
||||||
let name = info.name().to_string_lossy().to_string();
|
let name = info.name().to_string_lossy().into_owned();
|
||||||
let display_name = info.display_name().to_string();
|
let display_name = String::from(info.display_name());
|
||||||
|
|
||||||
let uri = file.child(info.name()).uri().to_string();
|
let uri = String::from(file.child(info.name()).uri());
|
||||||
|
|
||||||
//TODO: what is the best way to resolve shortcuts?
|
//TODO: what is the best way to resolve shortcuts?
|
||||||
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
|
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
|
||||||
|
|
@ -100,11 +102,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||||
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
|
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
|
||||||
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
||||||
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
|
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
|
||||||
let size_opt = if is_dir {
|
let size_opt = (!is_dir).then_some(info.size() as u64);
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(info.size() as u64)
|
|
||||||
};
|
|
||||||
let mut children_opt = None;
|
let mut children_opt = None;
|
||||||
|
|
||||||
if is_dir {
|
if is_dir {
|
||||||
|
|
@ -189,31 +187,21 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
|
||||||
move |mount_op, message, default_user, default_domain, flags| {
|
move |mount_op, message, default_user, default_domain, flags| {
|
||||||
let auth = MounterAuth {
|
let auth = MounterAuth {
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
username_opt: flags
|
||||||
Some(default_user.to_string())
|
.contains(gio::AskPasswordFlags::NEED_USERNAME)
|
||||||
} else {
|
.then(|| default_user.to_string()),
|
||||||
None
|
domain_opt: flags
|
||||||
},
|
.contains(gio::AskPasswordFlags::NEED_DOMAIN)
|
||||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
.then(|| default_domain.to_string()),
|
||||||
Some(default_domain.to_string())
|
password_opt: flags
|
||||||
} else {
|
.contains(gio::AskPasswordFlags::NEED_PASSWORD)
|
||||||
None
|
.then(String::new),
|
||||||
},
|
remember_opt: flags
|
||||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
.contains(gio::AskPasswordFlags::SAVING_SUPPORTED)
|
||||||
Some(String::new())
|
.then_some(false),
|
||||||
} else {
|
anonymous_opt: flags
|
||||||
None
|
.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED)
|
||||||
},
|
.then_some(false),
|
||||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||||
event_tx
|
event_tx
|
||||||
|
|
@ -458,7 +446,7 @@ impl Gvfs {
|
||||||
gio::Cancellable::NONE,
|
gio::Cancellable::NONE,
|
||||||
) {
|
) {
|
||||||
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
||||||
uri = resolved_uri.to_string();
|
uri = resolved_uri.into();
|
||||||
file = gio::File::for_uri(&uri);
|
file = gio::File::for_uri(&uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -597,12 +585,9 @@ impl Mounter for Gvfs {
|
||||||
|
|
||||||
fn unmount(&self, item: MounterItem) -> Task<()> {
|
fn unmount(&self, item: MounterItem) -> Task<()> {
|
||||||
let command_tx = self.command_tx.clone();
|
let command_tx = self.command_tx.clone();
|
||||||
Task::perform(
|
Task::future(async move {
|
||||||
async move {
|
|
||||||
command_tx.send(Cmd::Unmount(item)).unwrap();
|
command_tx.send(Cmd::Unmount(item)).unwrap();
|
||||||
},
|
})
|
||||||
|x| x,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<MounterMessage> {
|
fn subscription(&self) -> Subscription<MounterMessage> {
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,9 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
let file_name = file_name.to_string();
|
let file_name = file_name.to_string();
|
||||||
COMPOUND_EXTENSIONS
|
COMPOUND_EXTENSIONS
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&&ext| file_name.ends_with(ext))
|
.copied()
|
||||||
.map(|&ext| {
|
.find(|&ext| file_name.ends_with(ext))
|
||||||
|
.map(|ext| {
|
||||||
(
|
(
|
||||||
file_name.strip_suffix(ext).unwrap().to_string(),
|
file_name.strip_suffix(ext).unwrap().to_string(),
|
||||||
Some(ext[1..].to_string()),
|
Some(ext[1..].to_string()),
|
||||||
|
|
@ -251,7 +252,7 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
to = to.join(&new_name);
|
to.push(&new_name);
|
||||||
|
|
||||||
if !matches!(to.try_exists(), Ok(true)) {
|
if !matches!(to.try_exists(), Ok(true)) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -329,7 +330,7 @@ pub enum Operation {
|
||||||
EmptyTrash,
|
EmptyTrash,
|
||||||
/// Uncompress files
|
/// Uncompress files
|
||||||
Extract {
|
Extract {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
to: PathBuf,
|
to: PathBuf,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
@ -347,10 +348,10 @@ pub enum Operation {
|
||||||
},
|
},
|
||||||
/// Permanently delete items, skipping the trash
|
/// Permanently delete items, skipping the trash
|
||||||
PermanentlyDelete {
|
PermanentlyDelete {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
},
|
},
|
||||||
RemoveFromRecents {
|
RemoveFromRecents {
|
||||||
paths: Vec<PathBuf>,
|
paths: Box<[PathBuf]>,
|
||||||
},
|
},
|
||||||
Rename {
|
Rename {
|
||||||
from: PathBuf,
|
from: PathBuf,
|
||||||
|
|
@ -1013,7 +1014,7 @@ impl Operation {
|
||||||
}
|
}
|
||||||
Self::RemoveFromRecents { paths } => {
|
Self::RemoveFromRecents { paths } => {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let path_refs = paths.iter().map(PathBuf::as_path).collect::<Vec<&Path>>();
|
let path_refs = paths.iter().map(PathBuf::as_path).collect::<Box<[_]>>();
|
||||||
recently_used_xbel::remove_recently_used(&path_refs)
|
recently_used_xbel::remove_recently_used(&path_refs)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl Context {
|
||||||
|
|
||||||
pub async fn recursive_copy_or_move(
|
pub async fn recursive_copy_or_move(
|
||||||
&mut self,
|
&mut self,
|
||||||
from_to_pairs: Vec<(PathBuf, PathBuf)>,
|
from_to_pairs: impl IntoIterator<Item = (PathBuf, PathBuf)>,
|
||||||
method: Method,
|
method: Method,
|
||||||
) -> Result<bool, OperationError> {
|
) -> Result<bool, OperationError> {
|
||||||
let mut ops = Vec::new();
|
let mut ops = Vec::new();
|
||||||
|
|
@ -148,9 +148,8 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add cleanup ops after standard ops, in reverse
|
// Add cleanup ops after standard ops, in reverse
|
||||||
for cleanup_op in cleanup_ops.into_iter().rev() {
|
cleanup_ops.reverse();
|
||||||
ops.push(cleanup_op);
|
ops.append(&mut cleanup_ops);
|
||||||
}
|
|
||||||
|
|
||||||
let total_ops = ops.len();
|
let total_ops = ops.len();
|
||||||
for (current_ops, mut op) in ops.into_iter().enumerate() {
|
for (current_ops, mut op) in ops.into_iter().enumerate() {
|
||||||
|
|
|
||||||
483
src/tab.rs
483
src/tab.rs
|
|
@ -54,7 +54,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
cmp::Ordering,
|
cmp::{Ordering, Reverse},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
|
|
@ -483,18 +483,11 @@ impl Display for FormatTime<'_> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if datetime.date_naive() == now.date_naive() {
|
if datetime.date_naive() == now.date_naive() {
|
||||||
write!(
|
f.write_str(fl!("today").as_str())?;
|
||||||
f,
|
f.write_str(", ")?;
|
||||||
"{}, {}",
|
self.time_formatter.format(&icu_datetime).fmt(f)
|
||||||
fl!("today"),
|
|
||||||
self.time_formatter.format(&icu_datetime).to_string()
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
write!(
|
self.date_time_formatter.format(&icu_datetime).fmt(f)
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
self.date_time_formatter.format(&icu_datetime).to_string()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -606,11 +599,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE);
|
let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE);
|
||||||
let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);
|
let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);
|
||||||
|
|
||||||
let size_opt = if is_dir {
|
let size_opt = (!is_dir).then_some(file_info.size() as u64);
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(file_info.size() as u64)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if is_dir {
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = if is_dir {
|
||||||
(
|
(
|
||||||
|
|
@ -673,8 +662,10 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hidden = file_name.starts_with('.');
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
name: file_name.clone().to_string(),
|
name: file_name.into(),
|
||||||
display_name,
|
display_name,
|
||||||
is_mount_point: false,
|
is_mount_point: false,
|
||||||
metadata: ItemMetadata::GvfsPath {
|
metadata: ItemMetadata::GvfsPath {
|
||||||
|
|
@ -682,7 +673,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
size_opt,
|
size_opt,
|
||||||
children_opt,
|
children_opt,
|
||||||
},
|
},
|
||||||
hidden: file_name.starts_with('.'),
|
hidden,
|
||||||
location_opt: Some(Location::Path(path)),
|
location_opt: Some(Location::Path(path)),
|
||||||
mime,
|
mime,
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
|
|
@ -790,7 +781,7 @@ pub fn item_from_entry(
|
||||||
widget::icon::from_name(&*icon_name)
|
widget::icon::from_name(&*icon_name)
|
||||||
.size(sizes.list())
|
.size(sizes.list())
|
||||||
.handle(),
|
.handle(),
|
||||||
widget::icon::from_name(&*icon_name)
|
widget::icon::from_name(icon_name)
|
||||||
.size(sizes.list_condensed())
|
.size(sizes.list_condensed())
|
||||||
.handle(),
|
.handle(),
|
||||||
)
|
)
|
||||||
|
|
@ -833,11 +824,7 @@ pub fn item_from_entry(
|
||||||
icon_handle_grid,
|
icon_handle_grid,
|
||||||
icon_handle_list,
|
icon_handle_list,
|
||||||
icon_handle_list_condensed,
|
icon_handle_list_condensed,
|
||||||
thumbnail_opt: if remote {
|
thumbnail_opt: remote.then_some(ItemThumbnail::NotImage),
|
||||||
Some(ItemThumbnail::NotImage)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
button_id: widget::Id::unique(),
|
button_id: widget::Id::unique(),
|
||||||
pos_opt: Cell::new(None),
|
pos_opt: Cell::new(None),
|
||||||
rect_opt: Cell::new(None),
|
rect_opt: Cell::new(None),
|
||||||
|
|
@ -870,7 +857,7 @@ pub fn item_from_path<P: Into<PathBuf>>(path: P, sizes: IconSizes) -> Result<Ite
|
||||||
|
|
||||||
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut hidden_files = Vec::new();
|
let mut hidden_files = Box::from([]);
|
||||||
let mut remote_scannable = false;
|
let mut remote_scannable = false;
|
||||||
|
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
|
|
@ -880,19 +867,15 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
let file = gio::File::for_path(tab_path);
|
let file = gio::File::for_path(tab_path);
|
||||||
|
|
||||||
// gio crate expects a comma delimited string
|
// gio crate expects a comma delimited string
|
||||||
let mut attr_string = String::new();
|
let attr_string = [
|
||||||
for attr in [
|
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
|
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_TIME_MODIFIED,
|
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_STANDARD_SIZE,
|
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_STANDARD_TYPE,
|
gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),
|
||||||
gio::FILE_ATTRIBUTE_STANDARD_NAME,
|
]
|
||||||
] {
|
.join(",");
|
||||||
attr_string.push_str(attr);
|
|
||||||
attr_string.push(',');
|
|
||||||
}
|
|
||||||
attr_string.pop();
|
|
||||||
|
|
||||||
match gio::prelude::FileExt::enumerate_children(
|
match gio::prelude::FileExt::enumerate_children(
|
||||||
&file,
|
&file,
|
||||||
|
|
@ -902,10 +885,12 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
) {
|
) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
remote_scannable = true;
|
remote_scannable = true;
|
||||||
for file in res.flatten() {
|
items = res
|
||||||
let full_path = Path::new(tab_path).join(file.name());
|
.filter_map(|file| {
|
||||||
items.push(item_from_gvfs_info(full_path, file, sizes));
|
let file = file.ok()?;
|
||||||
}
|
Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -922,60 +907,62 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
if !remote_scannable {
|
if !remote_scannable {
|
||||||
match fs::read_dir(tab_path) {
|
match fs::read_dir(tab_path) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
for entry_res in entries {
|
items = entries
|
||||||
let entry = match entry_res {
|
.filter_map(|entry_res| {
|
||||||
Ok(ok) => ok,
|
let entry = entry_res
|
||||||
Err(err) => {
|
.inspect_err(|err| {
|
||||||
log::warn!("failed to read entry in {}: {}", tab_path.display(), err);
|
log::warn!(
|
||||||
continue;
|
"failed to read entry in {}: {}",
|
||||||
}
|
tab_path.display(),
|
||||||
};
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
let name = match entry.file_name().into_string() {
|
let name = entry
|
||||||
Ok(ok) => ok,
|
.file_name()
|
||||||
Err(name_os) => {
|
.into_string()
|
||||||
|
.inspect_err(|name_os| {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to parse entry at {}: {:?} is not valid UTF-8",
|
"failed to parse entry at {}: {:?} is not valid UTF-8",
|
||||||
path.display(),
|
path.display(),
|
||||||
name_os
|
name_os
|
||||||
);
|
)
|
||||||
continue;
|
})
|
||||||
}
|
.ok()?;
|
||||||
};
|
|
||||||
|
|
||||||
if name == ".hidden" && path.is_file() {
|
if name == ".hidden" && path.is_file() {
|
||||||
hidden_files = parse_hidden_file(&path);
|
hidden_files = parse_hidden_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = match fs::metadata(&path) {
|
let metadata = fs::metadata(&path)
|
||||||
Ok(ok) => ok,
|
.inspect_err(|err| {
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to read metadata for entry at {}: {}",
|
"failed to read metadata for entry at {}: {}",
|
||||||
path.display(),
|
path.display(),
|
||||||
err
|
err
|
||||||
);
|
)
|
||||||
continue;
|
})
|
||||||
}
|
.ok()?;
|
||||||
};
|
|
||||||
|
|
||||||
items.push(item_from_entry(path, name, metadata, sizes));
|
Some(item_from_entry(path, name, metadata, sizes))
|
||||||
}
|
})
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read directory {}: {}", tab_path.display(), err);
|
log::warn!("failed to read directory {}: {}", tab_path.display(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
items.sort_unstable_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||||
});
|
});
|
||||||
for item in &mut items {
|
for item in &mut items {
|
||||||
if hidden_files.iter().any(|hidden| &item.name == hidden) {
|
if hidden_files.contains(&item.name) {
|
||||||
item.hidden = true;
|
item.hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1074,20 +1061,23 @@ pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
|
||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut items: Vec<Item> = Vec::new();
|
let entries = match trash::os_limited::list() {
|
||||||
match trash::os_limited::list() {
|
Ok(entry) => entry,
|
||||||
Ok(entries) => {
|
|
||||||
for entry in entries {
|
|
||||||
let metadata = match trash::os_limited::metadata(&entry) {
|
|
||||||
Ok(ok) => ok,
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to get metadata for trash item {entry:?}: {err}");
|
log::warn!("failed to read trash items: {err}");
|
||||||
continue;
|
return Vec::new();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut items: Vec<_> = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let metadata = trash::os_limited::metadata(&entry)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
let original_path = entry.original_path();
|
let original_path = entry.original_path();
|
||||||
let name = entry.name.to_string_lossy().to_string();
|
let name = entry.name.to_string_lossy().into_owned();
|
||||||
let display_name = Item::display_name(&name);
|
let display_name = Item::display_name(&name);
|
||||||
|
|
||||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
||||||
|
|
@ -1111,7 +1101,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(Item {
|
Some(Item {
|
||||||
name,
|
name,
|
||||||
display_name,
|
display_name,
|
||||||
is_mount_point: false,
|
is_mount_point: false,
|
||||||
|
|
@ -1131,13 +1121,9 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
overlaps_drag_rect: false,
|
overlaps_drag_rect: false,
|
||||||
dir_size: DirSize::NotDirectory,
|
dir_size: DirSize::NotDirectory,
|
||||||
cut: false,
|
cut: false,
|
||||||
});
|
})
|
||||||
}
|
})
|
||||||
}
|
.collect();
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read trash items: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
|
|
@ -1158,31 +1144,24 @@ fn uri_to_path(uri: String) -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
||||||
let mut recents = Vec::new();
|
let recent_files = match recently_used_xbel::parse_file() {
|
||||||
|
Ok(recent_files) => recent_files,
|
||||||
match recently_used_xbel::parse_file() {
|
Err(err) => {
|
||||||
Ok(recent_files) => {
|
log::warn!("Error reading recent files: {err:?}");
|
||||||
for bookmark in recent_files.bookmarks {
|
return Vec::new();
|
||||||
let uri = bookmark.href;
|
}
|
||||||
let path = match uri_to_path(uri) {
|
|
||||||
None => continue,
|
|
||||||
Some(path) => path,
|
|
||||||
};
|
};
|
||||||
let last_edit = match bookmark.modified.parse::<chrono::DateTime<Utc>>() {
|
let mut recents: Vec<_> = recent_files
|
||||||
Ok(last_edit) => last_edit,
|
.bookmarks
|
||||||
Err(_) => continue,
|
.into_iter()
|
||||||
};
|
.filter_map(|bookmark| {
|
||||||
let last_visit = match bookmark.visited.parse::<chrono::DateTime<Utc>>() {
|
let path = uri_to_path(bookmark.href)?;
|
||||||
Ok(last_visit) => last_visit,
|
let last_edit = bookmark.modified.parse::<chrono::DateTime<Utc>>().ok()?;
|
||||||
Err(_) => continue,
|
let last_visit = bookmark.visited.parse::<chrono::DateTime<Utc>>().ok()?;
|
||||||
};
|
|
||||||
let path_exist = path.exists();
|
|
||||||
|
|
||||||
if path_exist {
|
if path.exists() {
|
||||||
let file_name = path.file_name();
|
let file_name = path.file_name()?;
|
||||||
|
let name = file_name.to_string_lossy().to_string();
|
||||||
if let Some(name) = file_name {
|
|
||||||
let name = name.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
let metadata = match path.metadata() {
|
let metadata = match path.metadata() {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
|
|
@ -1192,37 +1171,26 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
||||||
path.display(),
|
path.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
continue;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let item = item_from_entry(path, name, metadata, sizes);
|
let item = item_from_entry(path, name, metadata, sizes);
|
||||||
recents.push((
|
Some((item, last_edit.min(last_visit)))
|
||||||
item,
|
|
||||||
if last_edit.le(&last_visit) {
|
|
||||||
last_edit
|
|
||||||
} else {
|
|
||||||
last_visit
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log::warn!("recent file path not exist: {}", path.display());
|
log::warn!("recent file path not exist: {}", path.display());
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
.collect();
|
||||||
Err(err) => {
|
|
||||||
log::warn!("Error reading recent files: {err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recents.sort_by(|a, b| b.1.cmp(&a.1));
|
recents.sort_by_key(|recent| Reverse(recent.1));
|
||||||
|
|
||||||
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_network(uri: &str, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_network(uri: &str, sizes: IconSizes) -> Vec<Item> {
|
||||||
for (_key, mounter) in MOUNTERS.iter() {
|
for mounter in MOUNTERS.values() {
|
||||||
match mounter.network_scan(uri, sizes) {
|
match mounter.network_scan(uri, sizes) {
|
||||||
Some(Ok(items)) => return items,
|
Some(Ok(items)) => return items,
|
||||||
Some(Err(err)) => {
|
Some(Err(err)) => {
|
||||||
|
|
@ -1250,12 +1218,12 @@ pub fn scan_desktop(
|
||||||
}
|
}
|
||||||
|
|
||||||
if desktop_config.show_mounted_drives {
|
if desktop_config.show_mounted_drives {
|
||||||
for (_mounter_key, mounter) in MOUNTERS.iter() {
|
for mounter in MOUNTERS.values() {
|
||||||
for mounter_item in mounter.items(sizes).unwrap_or_default() {
|
let Some(mounter_items) = mounter.items(sizes) else {
|
||||||
let Some(path) = mounter_item.path() else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
items.extend(mounter_items.into_iter().filter_map(|mounter_item| {
|
||||||
|
let path = mounter_item.path()?;
|
||||||
// Get most item data from path
|
// Get most item data from path
|
||||||
let mut item = match item_from_path(&path, sizes) {
|
let mut item = match item_from_path(&path, sizes) {
|
||||||
Ok(item) => item,
|
Ok(item) => item,
|
||||||
|
|
@ -1265,7 +1233,7 @@ pub fn scan_desktop(
|
||||||
path.display(),
|
path.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
continue;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1275,13 +1243,13 @@ pub fn scan_desktop(
|
||||||
|
|
||||||
//TODO: use icon size for mounter item icon
|
//TODO: use icon size for mounter item icon
|
||||||
if let Some(icon) = mounter_item.icon(false) {
|
if let Some(icon) = mounter_item.icon(false) {
|
||||||
item.icon_handle_grid = icon.clone();
|
item.icon_handle_grid.clone_from(&icon);
|
||||||
item.icon_handle_list = icon.clone();
|
item.icon_handle_list.clone_from(&icon);
|
||||||
item.icon_handle_list_condensed = icon;
|
item.icon_handle_list_condensed = icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(item);
|
Some(item)
|
||||||
}
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1415,17 +1383,17 @@ impl Location {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ancestors(&self) -> Vec<(Self, String)> {
|
pub fn ancestors(&self) -> Vec<(Self, String)> {
|
||||||
let mut ancestors = Vec::new();
|
self.path_opt().map_or_else(Default::default, |path| {
|
||||||
if let Some(path) = self.path_opt() {
|
path.ancestors()
|
||||||
for ancestor in path.ancestors() {
|
.scan(false, |found_home, ancestor| {
|
||||||
let (name, found_home) = folder_name(ancestor);
|
(!*found_home).then(|| {
|
||||||
ancestors.push((self.with_path(ancestor.to_path_buf()), name));
|
let (name, is_home) = folder_name(ancestor);
|
||||||
if found_home {
|
*found_home = is_home;
|
||||||
break;
|
(self.with_path(ancestor.to_path_buf()), name)
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
}
|
.collect()
|
||||||
ancestors
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn path_opt(&self) -> Option<&PathBuf> {
|
pub const fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
|
|
@ -1438,6 +1406,16 @@ impl Location {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_path_opt(self) -> Option<PathBuf> {
|
||||||
|
match self {
|
||||||
|
Self::Desktop(path, ..) => Some(path),
|
||||||
|
Self::Path(path) => Some(path),
|
||||||
|
Self::Search(path, ..) => Some(path),
|
||||||
|
Self::Network(_, _, path) => path,
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_path(&self, path: PathBuf) -> Self {
|
pub fn with_path(&self, path: PathBuf) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(_, display, desktop_config) => {
|
Self::Desktop(_, display, desktop_config) => {
|
||||||
|
|
@ -1690,13 +1668,7 @@ impl ItemMetadata {
|
||||||
|
|
||||||
pub fn file_size(&self) -> Option<u64> {
|
pub fn file_size(&self) -> Option<u64> {
|
||||||
match self {
|
match self {
|
||||||
Self::Path { metadata, .. } => {
|
Self::Path { metadata, .. } => (!metadata.is_dir()).then_some(metadata.len()),
|
||||||
if metadata.is_dir() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(metadata.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Trash { metadata, .. } => match metadata.size {
|
Self::Trash { metadata, .. } => match metadata.size {
|
||||||
TrashItemSize::Bytes(size) => Some(size),
|
TrashItemSize::Bytes(size) => Some(size),
|
||||||
TrashItemSize::Entries(_) => None,
|
TrashItemSize::Entries(_) => None,
|
||||||
|
|
@ -2180,8 +2152,8 @@ impl Item {
|
||||||
#[cfg(feature = "gvfs")]
|
#[cfg(feature = "gvfs")]
|
||||||
ItemMetadata::GvfsPath { children_opt, .. } => {
|
ItemMetadata::GvfsPath { children_opt, .. } => {
|
||||||
// grab the fs::metadata object for gvfs paths since this is run on-demand
|
// grab the fs::metadata object for gvfs paths since this is run on-demand
|
||||||
if let Some(path) = &self.path_opt() {
|
if let Some(path) = self.path_opt() {
|
||||||
file_metadata = fs::metadata(*path).ok();
|
file_metadata = fs::metadata(path).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
dir_children_count = *children_opt;
|
dir_children_count = *children_opt;
|
||||||
|
|
@ -2320,9 +2292,7 @@ impl Item {
|
||||||
|
|
||||||
if !settings.is_empty() {
|
if !settings.is_empty() {
|
||||||
let mut section = widget::settings::section();
|
let mut section = widget::settings::section();
|
||||||
for setting in settings {
|
section = section.extend(settings);
|
||||||
section = section.add(setting);
|
|
||||||
}
|
|
||||||
column = column.push(section);
|
column = column.push(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2521,7 +2491,7 @@ fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
|
||||||
// This is not optimized but it helps ensure the same display names
|
// This is not optimized but it helps ensure the same display names
|
||||||
match item_from_path(path, IconSizes::default()) {
|
match item_from_path(path, IconSizes::default()) {
|
||||||
Ok(item) => item.display_name,
|
Ok(item) => item.display_name,
|
||||||
Err(_err) => name.to_string_lossy().to_string(),
|
Err(_err) => name.to_string_lossy().into_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2533,10 +2503,9 @@ fn folder_name<P: AsRef<Path>>(path: P) -> (String, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse .hidden file and return files path
|
// parse .hidden file and return files path
|
||||||
fn parse_hidden_file(path: &PathBuf) -> Vec<String> {
|
fn parse_hidden_file(path: &PathBuf) -> Box<[String]> {
|
||||||
let file = match File::open(path) {
|
let Ok(file) = File::open(path) else {
|
||||||
Ok(f) => f,
|
return Default::default();
|
||||||
Err(_) => return Vec::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BufReader::new(file)
|
BufReader::new(file)
|
||||||
|
|
@ -2645,11 +2614,9 @@ impl Tab {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
item.cut = false;
|
item.cut = false;
|
||||||
if let Some(location) = &item.location_opt {
|
if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt)
|
||||||
if locations
|
|
||||||
.iter()
|
|
||||||
.any(|s| location.path_opt().is_some_and(|b| b == s))
|
|
||||||
{
|
{
|
||||||
|
if locations.contains(location_path) {
|
||||||
item.cut = true;
|
item.cut = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2658,18 +2625,21 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_locations(&self) -> Vec<Location> {
|
pub fn selected_locations(&self) -> Vec<Location> {
|
||||||
let mut locations = Vec::new();
|
|
||||||
if let Some(ref items) = self.items_opt {
|
if let Some(ref items) = self.items_opt {
|
||||||
for item in items {
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| {
|
||||||
if item.selected {
|
if item.selected {
|
||||||
if let Some(location) = &item.location_opt {
|
item.location_opt.clone()
|
||||||
locations.push(location.clone());
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
locations
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_all(&mut self) {
|
pub fn select_all(&mut self) {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
|
@ -3065,13 +3035,15 @@ impl Tab {
|
||||||
// a sorted tab.
|
// a sorted tab.
|
||||||
let min = indices
|
let min = indices
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&offset| offset == range_min)
|
.copied()
|
||||||
|
.position(|offset| offset == range_min)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// We can't skip `min_real` elements here because the index of
|
// We can't skip `min_real` elements here because the index of
|
||||||
// `max` may actually be before `min` in a sorted tab
|
// `max` may actually be before `min` in a sorted tab
|
||||||
let max = indices
|
let max = indices
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&offset| offset == range_max)
|
.copied()
|
||||||
|
.position(|offset| offset == range_max)
|
||||||
.unwrap_or(indices.len());
|
.unwrap_or(indices.len());
|
||||||
let min_real = min.min(max);
|
let min_real = min.min(max);
|
||||||
let max_real = max.max(min);
|
let max_real = max.max(min);
|
||||||
|
|
@ -3096,7 +3068,7 @@ impl Tab {
|
||||||
let dont_unset = mod_ctrl
|
let dont_unset = mod_ctrl
|
||||||
|| self.column_sort().is_some_and(|l| {
|
|| self.column_sort().is_some_and(|l| {
|
||||||
l.iter()
|
l.iter()
|
||||||
.any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected)
|
.any(|&(e_i, e)| Some(e_i) == click_i_opt && e.selected)
|
||||||
});
|
});
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
for (i, item) in items.iter_mut().enumerate() {
|
for (i, item) in items.iter_mut().enumerate() {
|
||||||
|
|
@ -3291,7 +3263,7 @@ impl Tab {
|
||||||
match path.map_or_else(
|
match path.map_or_else(
|
||||||
|| {
|
|| {
|
||||||
let items = self.items_opt.as_deref()?;
|
let items = self.items_opt.as_deref()?;
|
||||||
items.iter().find(|item| item.selected).and_then(|item| {
|
items.iter().find(|&item| item.selected).and_then(|item| {
|
||||||
let location = item.location_opt.as_ref()?;
|
let location = item.location_opt.as_ref()?;
|
||||||
let path = location.path_opt()?;
|
let path = location.path_opt()?;
|
||||||
cosmic::desktop::load_desktop_file(&[language.into()], path.into())
|
cosmic::desktop::load_desktop_file(&[language.into()], path.into())
|
||||||
|
|
@ -3372,9 +3344,7 @@ impl Tab {
|
||||||
if let Some(edit_location) = &mut self.edit_location {
|
if let Some(edit_location) = &mut self.edit_location {
|
||||||
edit_location.select(true);
|
edit_location.select(true);
|
||||||
} else if self.gallery {
|
} else if self.gallery {
|
||||||
for command in self.update(Message::GalleryNext, modifiers) {
|
commands.append(&mut self.update(Message::GalleryNext, modifiers));
|
||||||
commands.push(command);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Some((row, col)) =
|
if let Some((row, col)) =
|
||||||
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
||||||
|
|
@ -3407,9 +3377,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Message::ItemLeft => {
|
Message::ItemLeft => {
|
||||||
if self.gallery {
|
if self.gallery {
|
||||||
for command in self.update(Message::GalleryPrevious, modifiers) {
|
commands.append(&mut self.update(Message::GalleryPrevious, modifiers));
|
||||||
commands.push(command);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Some((row, col)) =
|
if let Some((row, col)) =
|
||||||
self.select_focus_pos_opt().or(self.select_first_pos_opt())
|
self.select_focus_pos_opt().or(self.select_first_pos_opt())
|
||||||
|
|
@ -3460,9 +3428,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Message::ItemRight => {
|
Message::ItemRight => {
|
||||||
if self.gallery {
|
if self.gallery {
|
||||||
for command in self.update(Message::GalleryNext, modifiers) {
|
commands.append(&mut self.update(Message::GalleryNext, modifiers));
|
||||||
commands.push(command);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Some((row, col)) =
|
if let Some((row, col)) =
|
||||||
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
self.select_focus_pos_opt().or(self.select_last_pos_opt())
|
||||||
|
|
@ -3498,9 +3464,7 @@ impl Tab {
|
||||||
if let Some(edit_location) = &mut self.edit_location {
|
if let Some(edit_location) = &mut self.edit_location {
|
||||||
edit_location.select(false);
|
edit_location.select(false);
|
||||||
} else if self.gallery {
|
} else if self.gallery {
|
||||||
for command in self.update(Message::GalleryPrevious, modifiers) {
|
commands.append(&mut self.update(Message::GalleryPrevious, modifiers));
|
||||||
commands.push(command);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if let Some((row, col)) =
|
if let Some((row, col)) =
|
||||||
self.select_focus_pos_opt().or(self.select_first_pos_opt())
|
self.select_focus_pos_opt().or(self.select_first_pos_opt())
|
||||||
|
|
@ -3594,13 +3558,12 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Reload => {
|
Message::Reload => {
|
||||||
let mut selected_paths = Vec::new();
|
|
||||||
//TODO: support keeping selected locations without paths
|
//TODO: support keeping selected locations without paths
|
||||||
for location in self.selected_locations() {
|
let selected_paths = self
|
||||||
if let Some(path) = location.path_opt() {
|
.selected_locations()
|
||||||
selected_paths.push(path.clone());
|
.into_iter()
|
||||||
}
|
.filter_map(Location::into_path_opt)
|
||||||
}
|
.collect();
|
||||||
let location = self.location.clone();
|
let location = self.location.clone();
|
||||||
self.change_location(&location, None);
|
self.change_location(&location, None);
|
||||||
commands.push(Command::ChangeLocation(
|
commands.push(Command::ChangeLocation(
|
||||||
|
|
@ -3835,8 +3798,8 @@ impl Tab {
|
||||||
ItemThumbnail::Text(_text) => None,
|
ItemThumbnail::Text(_text) => None,
|
||||||
};
|
};
|
||||||
if let Some(handle) = handle_opt {
|
if let Some(handle) = handle_opt {
|
||||||
item.icon_handle_grid = handle.clone();
|
item.icon_handle_grid.clone_from(&handle);
|
||||||
item.icon_handle_list = handle.clone();
|
item.icon_handle_list.clone_from(&handle);
|
||||||
item.icon_handle_list_condensed = handle;
|
item.icon_handle_list_condensed = handle;
|
||||||
}
|
}
|
||||||
item.thumbnail_opt = Some(thumbnail);
|
item.thumbnail_opt = Some(thumbnail);
|
||||||
|
|
@ -3908,13 +3871,10 @@ impl Tab {
|
||||||
self.dnd_hovered = Some((loc.clone(), Instant::now()));
|
self.dnd_hovered = Some((loc.clone(), Instant::now()));
|
||||||
if loc != self.location {
|
if loc != self.location {
|
||||||
commands.push(Command::Iced(
|
commands.push(Command::Iced(
|
||||||
cosmic::Task::perform(
|
cosmic::Task::future(async move {
|
||||||
async move {
|
|
||||||
tokio::time::sleep(HOVER_DURATION).await;
|
tokio::time::sleep(HOVER_DURATION).await;
|
||||||
Message::DndHover(loc)
|
Message::DndHover(loc)
|
||||||
},
|
})
|
||||||
|x| x,
|
|
||||||
)
|
|
||||||
.into(),
|
.into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -3940,7 +3900,7 @@ impl Tab {
|
||||||
let location = Location::Path(path);
|
let location = Location::Path(path);
|
||||||
if let Some(ref mut item) = self.parent_item_opt {
|
if let Some(ref mut item) = self.parent_item_opt {
|
||||||
if item.location_opt.as_ref() == Some(&location) {
|
if item.location_opt.as_ref() == Some(&location) {
|
||||||
item.dir_size = dir_size.clone();
|
item.dir_size.clone_from(&dir_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
|
@ -4409,7 +4369,7 @@ impl Tab {
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let heading_row = widget::row::with_children(vec![
|
let heading_row = widget::row::with_children([
|
||||||
heading_item(fl!("name"), Length::Fill, HeadingOptions::Name),
|
heading_item(fl!("name"), Length::Fill, HeadingOptions::Name),
|
||||||
if self.location == Location::Trash {
|
if self.location == Location::Trash {
|
||||||
heading_item(
|
heading_item(
|
||||||
|
|
@ -4443,7 +4403,7 @@ impl Tab {
|
||||||
if let Some(edit_location) = &self.edit_location {
|
if let Some(edit_location) = &self.edit_location {
|
||||||
if let Some(location) = edit_location.resolve() {
|
if let Some(location) = edit_location.resolve() {
|
||||||
//TODO: allow editing other locations
|
//TODO: allow editing other locations
|
||||||
if let Some(path) = location.path_opt().cloned() {
|
if let Some(path) = location.path_opt() {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
widget::button::custom(
|
widget::button::custom(
|
||||||
widget::icon::from_name("window-close-symbolic").size(16),
|
widget::icon::from_name("window-close-symbolic").size(16),
|
||||||
|
|
@ -4452,7 +4412,7 @@ impl Tab {
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.class(theme::Button::Icon),
|
.class(theme::Button::Icon),
|
||||||
);
|
);
|
||||||
let text_input = widget::text_input("", path.to_string_lossy().to_string())
|
let text_input = widget::text_input("", path.to_string_lossy().into_owned())
|
||||||
.id(self.edit_location_id.clone())
|
.id(self.edit_location_id.clone())
|
||||||
.on_input(move |input| {
|
.on_input(move |input| {
|
||||||
Message::EditLocation(Some(
|
Message::EditLocation(Some(
|
||||||
|
|
@ -4632,9 +4592,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for child in children {
|
row = row.extend(children);
|
||||||
row = row.push(child);
|
|
||||||
}
|
|
||||||
let mut column = widget::column::with_capacity(4).padding([0, space_s]);
|
let mut column = widget::column::with_capacity(4).padding([0, space_s]);
|
||||||
column = column.push(row);
|
column = column.push(row);
|
||||||
column = column.push(accent_rule);
|
column = column.push(accent_rule);
|
||||||
|
|
@ -4663,10 +4621,9 @@ impl Tab {
|
||||||
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {
|
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
mouse_area::MouseArea::new(widget::column::with_children(vec![
|
mouse_area::MouseArea::new(widget::column::with_children([widget::container(
|
||||||
widget::container(
|
match self.mode {
|
||||||
widget::column::with_children(match self.mode {
|
Mode::App | Mode::Dialog(_) => widget::column::with_children([
|
||||||
Mode::App | Mode::Dialog(_) => vec![
|
|
||||||
widget::icon::from_name("folder-symbolic")
|
widget::icon::from_name("folder-symbolic")
|
||||||
.size(64)
|
.size(64)
|
||||||
.icon()
|
.icon()
|
||||||
|
|
@ -4679,15 +4636,14 @@ impl Tab {
|
||||||
fl!("empty-folder")
|
fl!("empty-folder")
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
],
|
]),
|
||||||
Mode::Desktop => Vec::new(),
|
Mode::Desktop => widget::column(),
|
||||||
})
|
}
|
||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.spacing(space_xxs),
|
.spacing(space_xxs),
|
||||||
)
|
)
|
||||||
.center(Length::Fill)
|
.center(Length::Fill)
|
||||||
.into(),
|
.into()]))
|
||||||
]))
|
|
||||||
.on_press(|_| Message::Click(None))
|
.on_press(|_| Message::Click(None))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
@ -4769,7 +4725,7 @@ impl Tab {
|
||||||
let mut drag_e_i = 0;
|
let mut drag_e_i = 0;
|
||||||
let mut drag_s_i = 0;
|
let mut drag_s_i = 0;
|
||||||
|
|
||||||
let mut children = Vec::new();
|
let mut column = widget::column::with_capacity(2);
|
||||||
if let Some(items) = self.column_sort() {
|
if let Some(items) = self.column_sort() {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut col = 0;
|
let mut col = 0;
|
||||||
|
|
@ -4921,7 +4877,7 @@ impl Tab {
|
||||||
return (None, self.empty_view(hidden > 0), false);
|
return (None, self.empty_view(hidden > 0), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(grid.into());
|
column = column.push(grid);
|
||||||
|
|
||||||
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
||||||
{
|
{
|
||||||
|
|
@ -4945,10 +4901,9 @@ impl Tab {
|
||||||
|
|
||||||
let spacer_height = height.saturating_sub(max_bottom + top_deduct);
|
let spacer_height = height.saturating_sub(max_bottom + top_deduct);
|
||||||
if spacer_height > 0 {
|
if spacer_height > 0 {
|
||||||
children.push(
|
column = column.push(widget::container(Space::with_height(Length::Fixed(
|
||||||
widget::container(Space::with_height(Length::Fixed(spacer_height as f32)))
|
spacer_height as f32,
|
||||||
.into(),
|
))));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4997,13 +4952,11 @@ impl Tab {
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut column = widget::column::with_capacity(buttons.len())
|
let column =
|
||||||
|
widget::column::with_children(buttons.into_iter().map(Element::from))
|
||||||
.align_x(Alignment::Center)
|
.align_x(Alignment::Center)
|
||||||
.height(Length::Fixed(item_height as f32))
|
.height(Length::Fixed(item_height as f32))
|
||||||
.width(Length::Fixed(item_width as f32));
|
.width(Length::Fixed(item_width as f32));
|
||||||
for button in buttons {
|
|
||||||
column = column.push(button)
|
|
||||||
}
|
|
||||||
|
|
||||||
dnd_grid = dnd_grid.push(column);
|
dnd_grid = dnd_grid.push(column);
|
||||||
dnd_item_i += 1;
|
dnd_item_i += 1;
|
||||||
|
|
@ -5018,8 +4971,7 @@ impl Tab {
|
||||||
Element::from(dnd_grid)
|
Element::from(dnd_grid)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut mouse_area =
|
let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill))
|
||||||
mouse_area::MouseArea::new(widget::column::with_children(children).width(Length::Fill))
|
|
||||||
.on_press(|_| Message::Click(None))
|
.on_press(|_| Message::Click(None))
|
||||||
.on_auto_scroll(Message::AutoScroll)
|
.on_auto_scroll(Message::AutoScroll)
|
||||||
.on_drag_end(|_| Message::DragEnd)
|
.on_drag_end(|_| Message::DragEnd)
|
||||||
|
|
@ -5063,7 +5015,7 @@ impl Tab {
|
||||||
};
|
};
|
||||||
let row_height = icon_size + 2 * space_xxs;
|
let row_height = icon_size + 2 * space_xxs;
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut column = widget::column::with_capacity(3);
|
||||||
let mut y: f32 = 0.0;
|
let mut y: f32 = 0.0;
|
||||||
|
|
||||||
let rule_padding = theme::active().cosmic().corner_radii.radius_xs[0] as u16;
|
let rule_padding = theme::active().cosmic().corner_radii.radius_xs[0] as u16;
|
||||||
|
|
@ -5091,11 +5043,8 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
children.push(
|
column = column
|
||||||
widget::container(horizontal_rule(1))
|
.push(widget::container(horizontal_rule(1)).padding([0, rule_padding]));
|
||||||
.padding([0, rule_padding])
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
y += 1.0;
|
y += 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5186,12 +5135,12 @@ impl Tab {
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = if condensed {
|
let row = if condensed {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
.into(),
|
.into(),
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(item.display_name.clone()).into(),
|
widget::text::body(item.display_name.clone()).into(),
|
||||||
//TODO: translate?
|
//TODO: translate?
|
||||||
widget::text::caption(format!("{modified_text} - {size_text}"))
|
widget::text::caption(format!("{modified_text} - {size_text}"))
|
||||||
|
|
@ -5203,12 +5152,12 @@ impl Tab {
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
} else if is_search {
|
} else if is_search {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
.into(),
|
.into(),
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(item.display_name.clone()).into(),
|
widget::text::body(item.display_name.clone()).into(),
|
||||||
widget::text::caption(match item.path_opt() {
|
widget::text::caption(match item.path_opt() {
|
||||||
Some(path) => path.display().to_string(),
|
Some(path) => path.display().to_string(),
|
||||||
|
|
@ -5229,7 +5178,7 @@ impl Tab {
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
} else {
|
} else {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list.clone())
|
widget::icon::icon(item.icon_handle_list.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
|
|
@ -5295,12 +5244,12 @@ impl Tab {
|
||||||
let dnd_row = if !item.selected {
|
let dnd_row = if !item.selected {
|
||||||
Element::from(Space::with_height(Length::Fixed(f32::from(row_height))))
|
Element::from(Space::with_height(Length::Fixed(f32::from(row_height))))
|
||||||
} else if condensed {
|
} else if condensed {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
.into(),
|
.into(),
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(item.display_name.clone()).into(),
|
widget::text::body(item.display_name.clone()).into(),
|
||||||
//TODO: translate?
|
//TODO: translate?
|
||||||
widget::text::body(format!("{modified_text} - {size_text}"))
|
widget::text::body(format!("{modified_text} - {size_text}"))
|
||||||
|
|
@ -5312,12 +5261,12 @@ impl Tab {
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.into()
|
.into()
|
||||||
} else if is_search {
|
} else if is_search {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
widget::icon::icon(item.icon_handle_list_condensed.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
.into(),
|
.into(),
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children([
|
||||||
widget::text::body(item.display_name.clone()).into(),
|
widget::text::body(item.display_name.clone()).into(),
|
||||||
widget::text::caption(match item.path_opt() {
|
widget::text::caption(match item.path_opt() {
|
||||||
Some(path) => path.display().to_string(),
|
Some(path) => path.display().to_string(),
|
||||||
|
|
@ -5338,7 +5287,7 @@ impl Tab {
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
widget::row::with_children(vec![
|
widget::row::with_children([
|
||||||
widget::icon::icon(item.icon_handle_list.clone())
|
widget::icon::icon(item.icon_handle_list.clone())
|
||||||
.content_fit(ContentFit::Contain)
|
.content_fit(ContentFit::Contain)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
|
|
@ -5378,7 +5327,7 @@ impl Tab {
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
y += f32::from(row_height);
|
y += f32::from(row_height);
|
||||||
children.push(button_row);
|
column = column.push(button_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
|
|
@ -5397,17 +5346,13 @@ impl Tab {
|
||||||
|
|
||||||
let spacer_height = size.height - y - f32::from(top_deduct);
|
let spacer_height = size.height - y - f32::from(top_deduct);
|
||||||
if spacer_height > 0. {
|
if spacer_height > 0. {
|
||||||
children.push(
|
column = column.push(widget::container(Space::with_height(spacer_height)));
|
||||||
widget::container(Space::with_height(Length::Fixed(spacer_height))).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let drag_col = (!drag_items.is_empty())
|
let drag_col = (!drag_items.is_empty())
|
||||||
.then(|| Element::from(widget::column::with_children(drag_items)));
|
.then(|| Element::from(widget::column::with_children(drag_items)));
|
||||||
|
|
||||||
let mut mouse_area = mouse_area::MouseArea::new(
|
let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s]))
|
||||||
widget::column::with_children(children).padding([0, space_s]),
|
|
||||||
)
|
|
||||||
.with_id(Id::new("list-view"))
|
.with_id(Id::new("list-view"))
|
||||||
.on_press(|_| Message::Click(None))
|
.on_press(|_| Message::Click(None))
|
||||||
.on_auto_scroll(Message::AutoScroll)
|
.on_auto_scroll(Message::AutoScroll)
|
||||||
|
|
@ -5452,9 +5397,14 @@ impl Tab {
|
||||||
.map(|items| {
|
.map(|items| {
|
||||||
items
|
items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|item| item.selected)
|
.filter_map(|item| {
|
||||||
.filter_map(|item| item.path_opt().cloned())
|
if item.selected {
|
||||||
.collect::<Vec<PathBuf>>()
|
item.path_opt().cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Box<[PathBuf]>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let item_view =
|
let item_view =
|
||||||
|
|
@ -5533,7 +5483,7 @@ impl Tab {
|
||||||
if let Some(items) = self.items_opt() {
|
if let Some(items) = self.items_opt() {
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
widget::layer_container(widget::row::with_children(vec![
|
widget::layer_container(widget::row::with_children([
|
||||||
widget::horizontal_space().into(),
|
widget::horizontal_space().into(),
|
||||||
widget::button::standard(fl!("empty-trash"))
|
widget::button::standard(fl!("empty-trash"))
|
||||||
.on_press(Message::EmptyTrash)
|
.on_press(Message::EmptyTrash)
|
||||||
|
|
@ -5549,7 +5499,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
Location::Network(uri, _display_name, _path) if uri == "network:///" => {
|
Location::Network(uri, _display_name, _path) if uri == "network:///" => {
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
widget::layer_container(widget::row::with_children(vec![
|
widget::layer_container(widget::row::with_children([
|
||||||
widget::horizontal_space().into(),
|
widget::horizontal_space().into(),
|
||||||
widget::button::standard(fl!("add-network-drive"))
|
widget::button::standard(fl!("add-network-drive"))
|
||||||
.on_press(Message::AddNetworkDrive)
|
.on_press(Message::AddNetworkDrive)
|
||||||
|
|
@ -5715,7 +5665,7 @@ impl Tab {
|
||||||
// Load directory size for selected items
|
// Load directory size for selected items
|
||||||
if let Some(item) = items
|
if let Some(item) = items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|item| item.selected)
|
.find(|&item| item.selected)
|
||||||
.or(self.parent_item_opt.as_ref())
|
.or(self.parent_item_opt.as_ref())
|
||||||
{
|
{
|
||||||
// Item must have a path
|
// Item must have a path
|
||||||
|
|
@ -5872,7 +5822,7 @@ impl Tab {
|
||||||
.cloned()
|
.cloned()
|
||||||
{
|
{
|
||||||
subscriptions.push(Subscription::run_with_id(
|
subscriptions.push(Subscription::run_with_id(
|
||||||
("tab_complete", path.to_string_lossy().to_string()),
|
("tab_complete", path.to_string_lossy().into_owned()),
|
||||||
stream::channel(1, |mut output| async move {
|
stream::channel(1, |mut output| async move {
|
||||||
let message = {
|
let message = {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
|
|
@ -6201,7 +6151,7 @@ mod tests {
|
||||||
let top_level = filter_dirs(path)?;
|
let top_level = filter_dirs(path)?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for dir in top_level {
|
for dir in top_level {
|
||||||
let nested_dirs: Vec<PathBuf> = filter_dirs(&dir)?.collect();
|
let nested_dirs = filter_dirs(&dir)?;
|
||||||
result.push(dir);
|
result.push(dir);
|
||||||
result.extend(nested_dirs);
|
result.extend(nested_dirs);
|
||||||
}
|
}
|
||||||
|
|
@ -6496,10 +6446,11 @@ mod tests {
|
||||||
debug!("Shuffled numbers for paths: {base_nums:?}");
|
debug!("Shuffled numbers for paths: {base_nums:?}");
|
||||||
let paths: Vec<_> = base_nums
|
let paths: Vec<_> = base_nums
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&base| path.join(std::iter::repeat_n(base, 255).collect::<String>()))
|
.copied()
|
||||||
|
.map(|base| path.join(std::iter::repeat_n(base, 255).collect::<String>()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (file, &base) in paths.iter().zip(base_nums.iter()) {
|
for (file, base) in paths.iter().zip(base_nums.into_iter()) {
|
||||||
trace!("Creating long file name for {base}");
|
trace!("Creating long file name for {base}");
|
||||||
fs::File::create(file)?;
|
fs::File::create(file)?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,9 +149,8 @@ impl ThumbnailCacher {
|
||||||
let info = reader.info();
|
let info = reader.info();
|
||||||
let text_chunks: FxHashMap<String, String> = info
|
let text_chunks: FxHashMap<String, String> = info
|
||||||
.uncompressed_latin1_text
|
.uncompressed_latin1_text
|
||||||
.clone()
|
.iter()
|
||||||
.into_iter()
|
.map(|chunk| (chunk.keyword.clone(), chunk.text.clone()))
|
||||||
.map(|chunk| (chunk.keyword, chunk.text))
|
|
||||||
.collect();
|
.collect();
|
||||||
(
|
(
|
||||||
info.width,
|
info.width,
|
||||||
|
|
@ -222,7 +221,7 @@ impl ThumbnailCacher {
|
||||||
// Thumb::URI is required and must match.
|
// Thumb::URI is required and must match.
|
||||||
let thumb_uri = texts
|
let thumb_uri = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::URI")
|
.find(|&text| text.keyword == "Thumb::URI")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_uri) = thumb_uri {
|
if let Some(thumb_uri) = thumb_uri {
|
||||||
if *thumb_uri != self.file_uri {
|
if *thumb_uri != self.file_uri {
|
||||||
|
|
@ -247,7 +246,7 @@ impl ThumbnailCacher {
|
||||||
// Thumb::MTime is required and must match.
|
// Thumb::MTime is required and must match.
|
||||||
let thumb_mtime = texts
|
let thumb_mtime = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::MTime")
|
.find(|&text| text.keyword == "Thumb::MTime")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_mtime) = thumb_mtime {
|
if let Some(thumb_mtime) = thumb_mtime {
|
||||||
let modified = match metadata.modified() {
|
let modified = match metadata.modified() {
|
||||||
|
|
@ -276,7 +275,7 @@ impl ThumbnailCacher {
|
||||||
// Thumb::Size isn't required, but it should be verified if present.
|
// Thumb::Size isn't required, but it should be verified if present.
|
||||||
let thumb_size = texts
|
let thumb_size = texts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|text| text.keyword == "Thumb::Size")
|
.find(|&text| text.keyword == "Thumb::Size")
|
||||||
.map(|t| &t.text);
|
.map(|t| &t.text);
|
||||||
if let Some(thumb_size) = thumb_size {
|
if let Some(thumb_size) = thumb_size {
|
||||||
let size = metadata.len();
|
let size = metadata.len();
|
||||||
|
|
@ -301,7 +300,7 @@ fn thumbnail_uri(path: &Path) -> io::Result<String> {
|
||||||
// and they aren't by the url crate, but the thumbnailer used by
|
// and they aren't by the url crate, but the thumbnailer used by
|
||||||
// Gnome Files does. In order to share thumbnails and not get duplicates
|
// Gnome Files does. In order to share thumbnails and not get duplicates
|
||||||
// we should do the same.
|
// we should do the same.
|
||||||
let url = url.to_string().replace('[', "%5B").replace(']', "%5D");
|
let url = url.as_str().replace('[', "%5B").replace(']', "%5D");
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,30 +80,32 @@ impl ThumbnailerCache {
|
||||||
let mut search_dirs = Vec::new();
|
let mut search_dirs = Vec::new();
|
||||||
let xdg_dirs = xdg::BaseDirectories::new();
|
let xdg_dirs = xdg::BaseDirectories::new();
|
||||||
|
|
||||||
if let Some(data_home) = xdg_dirs.get_data_home() {
|
if let Some(mut data_home) = xdg_dirs.get_data_home() {
|
||||||
search_dirs.push(data_home.join("thumbnailers"));
|
data_home.push("thumbnailers");
|
||||||
}
|
search_dirs.push(data_home);
|
||||||
for data_dir in xdg_dirs.get_data_dirs() {
|
|
||||||
search_dirs.push(data_dir.join("thumbnailers"));
|
|
||||||
}
|
}
|
||||||
|
search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| {
|
||||||
|
data_dir.push("thumbnailers");
|
||||||
|
data_dir
|
||||||
|
}));
|
||||||
|
|
||||||
let mut thumbnailer_paths = Vec::new();
|
let mut thumbnailer_paths = Vec::new();
|
||||||
for dir in search_dirs {
|
for dir in search_dirs {
|
||||||
log::trace!("looking for thumbnailers in {}", dir.display());
|
log::trace!("looking for thumbnailers in {}", dir.display());
|
||||||
match fs::read_dir(&dir) {
|
match fs::read_dir(&dir) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
for entry_res in entries {
|
thumbnailer_paths.extend(entries.filter_map(|entry_res| {
|
||||||
match entry_res {
|
entry_res
|
||||||
Ok(entry) => thumbnailer_paths.push(entry.path()),
|
.inspect_err(|err| {
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to read entry in directory {}: {}",
|
"failed to read entry in directory {}: {}",
|
||||||
dir.display(),
|
dir.display(),
|
||||||
err
|
err
|
||||||
);
|
)
|
||||||
}
|
})
|
||||||
}
|
.ok()
|
||||||
}
|
.map(|entry| entry.path())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read directory {}: {}", dir.display(), err);
|
log::warn!("failed to read directory {}: {}", dir.display(), err);
|
||||||
|
|
|
||||||
52
src/zoom.rs
Normal file
52
src/zoom.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
use std::num::NonZeroU16;
|
||||||
|
|
||||||
|
use crate::{config::IconSizes, tab::View};
|
||||||
|
|
||||||
|
static DEFAULT_ZOOM: NonZeroU16 = NonZeroU16::new(100).unwrap();
|
||||||
|
static MIN_ZOOM: NonZeroU16 = NonZeroU16::new(50).unwrap();
|
||||||
|
static MAX_ZOOM: NonZeroU16 = NonZeroU16::new(500).unwrap();
|
||||||
|
const ZOOM_STEP: u16 = 25;
|
||||||
|
|
||||||
|
pub(crate) const fn zoom_to_default(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
*icon_size = DEFAULT_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn zoom_in_view(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
|
||||||
|
let mut step = MIN_ZOOM;
|
||||||
|
while step <= MAX_ZOOM {
|
||||||
|
if *icon_size < step {
|
||||||
|
*icon_size = step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
step = step.saturating_add(ZOOM_STEP);
|
||||||
|
}
|
||||||
|
if *icon_size > step {
|
||||||
|
*icon_size = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn zoom_out_view(view: View, icon_sizes: &mut IconSizes) {
|
||||||
|
let icon_size = select_resized_icon(view, icon_sizes);
|
||||||
|
|
||||||
|
let mut step = MAX_ZOOM;
|
||||||
|
while step >= MIN_ZOOM {
|
||||||
|
if *icon_size > step {
|
||||||
|
*icon_size = step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
step = NonZeroU16::new(step.get().saturating_sub(ZOOM_STEP)).unwrap();
|
||||||
|
}
|
||||||
|
if *icon_size < step {
|
||||||
|
*icon_size = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn select_resized_icon(view: View, icon_sizes: &mut IconSizes) -> &mut NonZeroU16 {
|
||||||
|
match view {
|
||||||
|
View::Grid => &mut icon_sizes.grid,
|
||||||
|
View::List => &mut icon_sizes.list,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue