feat: search in Recents and Trash
This commit is contained in:
parent
49e3d95e7a
commit
bba95c3fc0
4 changed files with 459 additions and 275 deletions
111
src/app.rs
111
src/app.rs
|
|
@ -94,7 +94,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
spawn_detached::spawn_detached,
|
spawn_detached::spawn_detached,
|
||||||
tab::{
|
tab::{
|
||||||
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, Tab,
|
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,
|
||||||
|
SearchLocation, Tab,
|
||||||
},
|
},
|
||||||
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
||||||
};
|
};
|
||||||
|
|
@ -1384,7 +1385,9 @@ impl App {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
let tab = self.tab_model.data::<Tab>(entity)?;
|
let tab = self.tab_model.data::<Tab>(entity)?;
|
||||||
(tab.location == Location::Trash).then_some((entity, Location::Trash))
|
tab.location
|
||||||
|
.is_trash()
|
||||||
|
.then_some((entity, tab.location.clone()))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -1395,31 +1398,15 @@ impl App {
|
||||||
Task::batch(commands)
|
Task::batch(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh all tabs that are opened in [`Location::Recents`].
|
|
||||||
fn refresh_recents_tabs(&mut self) -> Task<Message> {
|
|
||||||
let commands: Box<[_]> = self
|
|
||||||
.tab_model
|
|
||||||
.iter()
|
|
||||||
.filter_map(|entity| {
|
|
||||||
let tab = self.tab_model.data::<Tab>(entity)?;
|
|
||||||
(tab.location == Location::Recents).then_some(entity)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let commands = commands
|
|
||||||
.into_iter()
|
|
||||||
.map(|entity| self.update_tab(entity, Location::Recents, None));
|
|
||||||
|
|
||||||
Task::batch(commands)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rescan_recents(&mut self) -> Task<Message> {
|
fn rescan_recents(&mut self) -> Task<Message> {
|
||||||
let needs_reload: Box<[_]> = self
|
let needs_reload: Box<[_]> = self
|
||||||
.tab_model
|
.tab_model
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
let tab = self.tab_model.data::<Tab>(entity)?;
|
let tab = self.tab_model.data::<Tab>(entity)?;
|
||||||
(tab.location == Location::Recents).then_some((entity, Location::Recents))
|
tab.location
|
||||||
|
.is_recents()
|
||||||
|
.then_some((entity, tab.location.clone()))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -1453,19 +1440,35 @@ impl App {
|
||||||
let mut title_location_opt = None;
|
let mut title_location_opt = None;
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) {
|
||||||
let location_opt = match term_opt {
|
let location_opt = match term_opt {
|
||||||
Some(term) => tab.location.path_opt().map(|path| {
|
Some(term) => {
|
||||||
(
|
let search_location = if let Some(path) = tab.location.path_opt() {
|
||||||
Location::Search(
|
Some(SearchLocation::Path(path.clone()))
|
||||||
path.clone(),
|
} else if tab.location.is_recents() {
|
||||||
term,
|
Some(SearchLocation::Recents)
|
||||||
tab.config.show_hidden,
|
} else if tab.location.is_trash() {
|
||||||
Instant::now(),
|
Some(SearchLocation::Trash)
|
||||||
),
|
} else {
|
||||||
true,
|
None
|
||||||
)
|
};
|
||||||
}),
|
|
||||||
|
search_location.map(|search_location| {
|
||||||
|
return (
|
||||||
|
Location::Search(
|
||||||
|
search_location,
|
||||||
|
term,
|
||||||
|
tab.config.show_hidden,
|
||||||
|
Instant::now(),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
None => match &tab.location {
|
None => match &tab.location {
|
||||||
Location::Search(path, ..) => Some((Location::Path(path.clone()), false)),
|
Location::Search(search_location, ..) => match search_location {
|
||||||
|
SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)),
|
||||||
|
SearchLocation::Recents => Some((Location::Recents, false)),
|
||||||
|
SearchLocation::Trash => Some((Location::Trash, false)),
|
||||||
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1625,7 +1628,7 @@ impl App {
|
||||||
|
|
||||||
nav_model = nav_model.insert(|b| {
|
nav_model = nav_model.insert(|b| {
|
||||||
b.text(fl!("trash"))
|
b.text(fl!("trash"))
|
||||||
.icon(icon::icon(tab::trash_icon_symbolic(16)))
|
.icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
|
||||||
.data(Location::Trash)
|
.data(Location::Trash)
|
||||||
.divider_above()
|
.divider_above()
|
||||||
});
|
});
|
||||||
|
|
@ -2840,7 +2843,7 @@ impl Application for App {
|
||||||
Message::Delete(entity_opt) => {
|
Message::Delete(entity_opt) => {
|
||||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
if tab.location == Location::Trash {
|
if tab.location.is_trash() {
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
let mut trash_items = Vec::new();
|
let mut trash_items = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
|
|
@ -4056,7 +4059,7 @@ impl Application for App {
|
||||||
return self.operation(Operation::RemoveFromRecents { paths });
|
return self.operation(Operation::RemoveFromRecents { paths });
|
||||||
}
|
}
|
||||||
Message::RescanRecents => {
|
Message::RescanRecents => {
|
||||||
return self.refresh_recents_tabs();
|
return self.rescan_recents();
|
||||||
}
|
}
|
||||||
Message::RescanTrash => {
|
Message::RescanTrash => {
|
||||||
// Update trash icon if empty/full
|
// Update trash icon if empty/full
|
||||||
|
|
@ -4066,8 +4069,10 @@ impl Application for App {
|
||||||
.is_some_and(|loc| matches!(loc, Location::Trash))
|
.is_some_and(|loc| matches!(loc, Location::Trash))
|
||||||
});
|
});
|
||||||
if let Some(entity) = maybe_entity {
|
if let Some(entity) = maybe_entity {
|
||||||
self.nav_model
|
self.nav_model.icon_set(
|
||||||
.icon_set(entity, icon::icon(tab::trash_icon_symbolic(16)));
|
entity,
|
||||||
|
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task::batch([self.rescan_trash(), self.update_desktop()]);
|
return Task::batch([self.rescan_trash(), self.update_desktop()]);
|
||||||
|
|
@ -4659,17 +4664,17 @@ impl Application for App {
|
||||||
Some(
|
Some(
|
||||||
Location::Desktop(path, ..)
|
Location::Desktop(path, ..)
|
||||||
| Location::Path(path)
|
| Location::Path(path)
|
||||||
| Location::Search(path, ..),
|
| Location::Search(SearchLocation::Path(path), ..),
|
||||||
) => {
|
) => {
|
||||||
command.arg(path);
|
command.arg(path);
|
||||||
}
|
}
|
||||||
Some(Location::Network(uri, ..)) => {
|
Some(Location::Network(uri, ..)) => {
|
||||||
command.arg(uri);
|
command.arg(uri);
|
||||||
}
|
}
|
||||||
Some(Location::Recents) => {
|
Some(Location::Recents | Location::Search(SearchLocation::Recents, ..)) => {
|
||||||
command.arg("--recents");
|
command.arg("--recents");
|
||||||
}
|
}
|
||||||
Some(Location::Trash) => {
|
Some(Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {
|
||||||
command.arg("--trash");
|
command.arg("--trash");
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
|
|
@ -5915,21 +5920,19 @@ impl Application for App {
|
||||||
dialog
|
dialog
|
||||||
.control(
|
.control(
|
||||||
widget::checkbox(
|
widget::checkbox(
|
||||||
format!("{} ({})" ,fl!("apply-to-all"), *conflict_count),
|
format!("{} ({})", fl!("apply-to-all"), *conflict_count),
|
||||||
*apply_to_all,
|
*apply_to_all,
|
||||||
)
|
)
|
||||||
.on_toggle(
|
.on_toggle(|apply_to_all| {
|
||||||
|apply_to_all| {
|
Message::DialogUpdate(DialogPage::Replace {
|
||||||
Message::DialogUpdate(DialogPage::Replace {
|
from: from.clone(),
|
||||||
from: from.clone(),
|
to: to.clone(),
|
||||||
to: to.clone(),
|
multiple: *multiple,
|
||||||
multiple: *multiple,
|
apply_to_all,
|
||||||
apply_to_all,
|
conflict_count: *conflict_count,
|
||||||
conflict_count: *conflict_count,
|
tx: tx.clone(),
|
||||||
tx: tx.clone(),
|
})
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.secondary_action(
|
.secondary_action(
|
||||||
widget::button::standard(fl!("skip")).on_press(Message::ReplaceResult(
|
widget::button::standard(fl!("skip")).on_press(Message::ReplaceResult(
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ use crate::{
|
||||||
localize::LANGUAGE_SORTER,
|
localize::LANGUAGE_SORTER,
|
||||||
menu,
|
menu,
|
||||||
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage},
|
||||||
tab::{self, ItemMetadata, Location, Tab},
|
tab::{self, ItemMetadata, Location, SearchLocation, Tab},
|
||||||
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -780,19 +780,35 @@ impl App {
|
||||||
|
|
||||||
fn search_set(&mut self, term_opt: Option<String>) -> Task<Message> {
|
fn search_set(&mut self, term_opt: Option<String>) -> Task<Message> {
|
||||||
let location_opt = match term_opt {
|
let location_opt = match term_opt {
|
||||||
Some(term) => self.tab.location.path_opt().map(|path| {
|
Some(term) => {
|
||||||
(
|
let search_location = if let Some(path) = self.tab.location.path_opt() {
|
||||||
Location::Search(
|
Some(SearchLocation::Path(path.clone()))
|
||||||
path.clone(),
|
} else if self.tab.location.is_recents() {
|
||||||
term,
|
Some(SearchLocation::Recents)
|
||||||
self.tab.config.show_hidden,
|
} else if self.tab.location.is_trash() {
|
||||||
Instant::now(),
|
Some(SearchLocation::Trash)
|
||||||
),
|
} else {
|
||||||
true,
|
None
|
||||||
)
|
};
|
||||||
}),
|
|
||||||
|
search_location.map(|search_location| {
|
||||||
|
return (
|
||||||
|
Location::Search(
|
||||||
|
search_location,
|
||||||
|
term,
|
||||||
|
self.tab.config.show_hidden,
|
||||||
|
Instant::now(),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
None => match &self.tab.location {
|
None => match &self.tab.location {
|
||||||
Location::Search(path, ..) => Some((Location::Path(path.clone()), false)),
|
Location::Search(search_location, ..) => match search_location {
|
||||||
|
SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)),
|
||||||
|
SearchLocation::Recents => Some((Location::Recents, false)),
|
||||||
|
SearchLocation::Trash => Some((Location::Trash, false)),
|
||||||
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
24
src/menu.rs
24
src/menu.rs
|
|
@ -22,7 +22,7 @@ use crate::{
|
||||||
app::{Action, Message},
|
app::{Action, Message},
|
||||||
config::Config,
|
config::Config,
|
||||||
fl,
|
fl,
|
||||||
tab::{self, HeadingOptions, Location, LocationMenuAction, Tab},
|
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
|
||||||
};
|
};
|
||||||
|
|
||||||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||||
|
|
@ -138,7 +138,9 @@ pub fn context_menu<'a>(
|
||||||
selected_dir += 1;
|
selected_dir += 1;
|
||||||
}
|
}
|
||||||
match &item.location_opt {
|
match &item.location_opt {
|
||||||
Some(Location::Trash) => selected_trash_only = true,
|
Some(Location::Trash) | Some(Location::Search(SearchLocation::Trash, ..)) => {
|
||||||
|
selected_trash_only = true
|
||||||
|
}
|
||||||
Some(Location::Path(path)) => {
|
Some(Location::Path(path)) => {
|
||||||
if selected == 1
|
if selected == 1
|
||||||
&& path.extension().and_then(|s| s.to_str()) == Some("desktop")
|
&& path.extension().and_then(|s| s.to_str()) == Some("desktop")
|
||||||
|
|
@ -174,7 +176,8 @@ pub fn context_menu<'a>(
|
||||||
tab::Mode::App | tab::Mode::Desktop,
|
tab::Mode::App | tab::Mode::Desktop,
|
||||||
Location::Desktop(..)
|
Location::Desktop(..)
|
||||||
| Location::Path(..)
|
| Location::Path(..)
|
||||||
| Location::Search(..)
|
| Location::Search(SearchLocation::Path(..), ..)
|
||||||
|
| Location::Search(SearchLocation::Recents, ..)
|
||||||
| Location::Recents
|
| Location::Recents
|
||||||
| Location::Network(_, _, Some(_)),
|
| Location::Network(_, _, Some(_)),
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -212,7 +215,7 @@ pub fn context_menu<'a>(
|
||||||
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matches!(tab.location, Location::Search(..) | Location::Recents) {
|
if tab.location.is_recents() {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||||
);
|
);
|
||||||
|
|
@ -255,7 +258,7 @@ pub fn context_menu<'a>(
|
||||||
children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
|
children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
|
||||||
}
|
}
|
||||||
children.push(divider::horizontal::light().into());
|
children.push(divider::horizontal::light().into());
|
||||||
if matches!(tab.location, Location::Recents) {
|
if tab.location.is_recents() {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
|
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(),
|
||||||
);
|
);
|
||||||
|
|
@ -322,7 +325,8 @@ pub fn context_menu<'a>(
|
||||||
tab::Mode::Dialog(dialog_kind),
|
tab::Mode::Dialog(dialog_kind),
|
||||||
Location::Desktop(..)
|
Location::Desktop(..)
|
||||||
| Location::Path(..)
|
| Location::Path(..)
|
||||||
| Location::Search(..)
|
| Location::Search(SearchLocation::Path(..), ..)
|
||||||
|
| Location::Search(SearchLocation::Recents, ..)
|
||||||
| Location::Recents
|
| Location::Recents
|
||||||
| Location::Network(_, _, Some(_)),
|
| Location::Network(_, _, Some(_)),
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -330,7 +334,7 @@ pub fn context_menu<'a>(
|
||||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||||
}
|
}
|
||||||
if matches!(tab.location, Location::Search(..) | Location::Recents) {
|
if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||||
);
|
);
|
||||||
|
|
@ -369,7 +373,7 @@ pub fn context_menu<'a>(
|
||||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(_, Location::Trash) => {
|
(_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {
|
||||||
if tab.mode.multiple() {
|
if tab.mode.multiple() {
|
||||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||||
}
|
}
|
||||||
|
|
@ -428,7 +432,7 @@ pub fn dialog_menu(
|
||||||
Action::SetSort(sort, dir),
|
Action::SetSort(sort, dir),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let in_trash = tab.location == Location::Trash;
|
let in_trash = tab.location.is_trash();
|
||||||
|
|
||||||
let mut selected_gallery = 0;
|
let mut selected_gallery = 0;
|
||||||
if let Some(items) = tab.items_opt() {
|
if let Some(items) = tab.items_opt() {
|
||||||
|
|
@ -578,7 +582,7 @@ pub fn menu_bar<'a>(
|
||||||
Action::SetSort(sort, dir),
|
Action::SetSort(sort, dir),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let in_trash = tab_opt.is_some_and(|tab| tab.location == Location::Trash);
|
let in_trash = tab_opt.is_some_and(|tab| tab.location.is_trash());
|
||||||
|
|
||||||
let mut selected_dir = 0;
|
let mut selected_dir = 0;
|
||||||
let mut selected = 0;
|
let mut selected = 0;
|
||||||
|
|
|
||||||
557
src/tab.rs
557
src/tab.rs
|
|
@ -52,6 +52,7 @@ use icu::{
|
||||||
use image::{DynamicImage, ImageDecoder, ImageReader};
|
use image::{DynamicImage, ImageDecoder, ImageReader};
|
||||||
use jxl_oxide::integration::JxlDecoder;
|
use jxl_oxide::integration::JxlDecoder;
|
||||||
use mime_guess::{Mime, mime};
|
use mime_guess::{Mime, mime};
|
||||||
|
use regex::Regex;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -72,7 +73,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use trash::TrashItemSize;
|
use trash::{TrashItem, TrashItemMetadata, TrashItemSize};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -362,39 +363,6 @@ fn tab_complete(path: &Path) -> Result<Vec<(String, PathBuf)>, Box<dyn Error>> {
|
||||||
Ok(completions)
|
Ok(completions)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pub fn trash_entries() -> usize {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
pub fn trash_entries() -> usize {
|
|
||||||
match trash::os_limited::list() {
|
|
||||||
Ok(entries) => entries.len(),
|
|
||||||
Err(_err) => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
|
||||||
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
|
||||||
"user-trash"
|
|
||||||
} else {
|
|
||||||
"user-trash-full"
|
|
||||||
})
|
|
||||||
.size(icon_size)
|
|
||||||
.handle()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
|
||||||
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
|
||||||
"user-trash-symbolic"
|
|
||||||
} else {
|
|
||||||
"user-trash-full-symbolic"
|
|
||||||
})
|
|
||||||
.size(icon_size)
|
|
||||||
.handle()
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: translate, add more levels?
|
//TODO: translate, add more levels?
|
||||||
fn format_size(size: u64) -> String {
|
fn format_size(size: u64) -> String {
|
||||||
const KB: u64 = 1000;
|
const KB: u64 = 1000;
|
||||||
|
|
@ -787,6 +755,13 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn item_from_search_item(search_item: SearchItem, sizes: IconSizes) -> Item {
|
||||||
|
match search_item {
|
||||||
|
SearchItem::Path(path, name, metadata) => item_from_entry(path, name, metadata, sizes),
|
||||||
|
SearchItem::Trash(entry, metadata) => item_from_trash_entry(entry, metadata, sizes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn item_from_entry(
|
pub fn item_from_entry(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -910,6 +885,59 @@ pub fn item_from_entry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn item_from_trash_entry(
|
||||||
|
entry: TrashItem,
|
||||||
|
metadata: TrashItemMetadata,
|
||||||
|
sizes: IconSizes,
|
||||||
|
) -> Item {
|
||||||
|
let original_path = entry.original_path();
|
||||||
|
let name = entry.name.to_string_lossy().into_owned();
|
||||||
|
let display_name = Item::display_name(&name);
|
||||||
|
|
||||||
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = match metadata.size
|
||||||
|
{
|
||||||
|
trash::TrashItemSize::Entries(_) => (
|
||||||
|
//TODO: make this a static
|
||||||
|
"inode/directory".parse().unwrap(),
|
||||||
|
folder_icon(&original_path, sizes.grid()),
|
||||||
|
folder_icon(&original_path, sizes.list()),
|
||||||
|
folder_icon(&original_path, sizes.list_condensed()),
|
||||||
|
),
|
||||||
|
trash::TrashItemSize::Bytes(_) => {
|
||||||
|
// This passes remote = true so it does not read from the original path
|
||||||
|
let mime = mime_for_path(&original_path, None, true);
|
||||||
|
(
|
||||||
|
mime.clone(),
|
||||||
|
mime_icon(mime.clone(), sizes.grid()),
|
||||||
|
mime_icon(mime.clone(), sizes.list()),
|
||||||
|
mime_icon(mime, sizes.list_condensed()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Item {
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
is_mount_point: false,
|
||||||
|
metadata: ItemMetadata::Trash { metadata, entry },
|
||||||
|
hidden: false,
|
||||||
|
location_opt: None,
|
||||||
|
mime,
|
||||||
|
icon_handle_grid,
|
||||||
|
icon_handle_list,
|
||||||
|
icon_handle_list_condensed,
|
||||||
|
thumbnail_opt: Some(ItemThumbnail::NotImage),
|
||||||
|
button_id: widget::Id::unique(),
|
||||||
|
pos_opt: Cell::new(None),
|
||||||
|
rect_opt: Cell::new(None),
|
||||||
|
selected: false,
|
||||||
|
highlighted: false,
|
||||||
|
overlaps_drag_rect: false,
|
||||||
|
dir_size: DirSize::NotDirectory,
|
||||||
|
cut: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_filename_from_path(path: &Path) -> Result<String, String> {
|
fn get_filename_from_path(path: &Path) -> Result<String, String> {
|
||||||
Ok(match path.file_name() {
|
Ok(match path.file_name() {
|
||||||
Some(name_os) => name_os
|
Some(name_os) => name_os
|
||||||
|
|
@ -1047,8 +1075,8 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_search<F: Fn(&Path, &str, Metadata) -> bool + Sync>(
|
pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
|
||||||
tab_path: &PathBuf,
|
search_location: &SearchLocation,
|
||||||
term: &str,
|
term: &str,
|
||||||
show_hidden: bool,
|
show_hidden: bool,
|
||||||
callback: F,
|
callback: F,
|
||||||
|
|
@ -1069,48 +1097,99 @@ pub fn scan_search<F: Fn(&Path, &str, Metadata) -> bool + Sync>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ignore::WalkBuilder::new(tab_path)
|
match search_location {
|
||||||
.standard_filters(false)
|
SearchLocation::Path(tab_path) => {
|
||||||
.hidden(!show_hidden)
|
ignore::WalkBuilder::new(tab_path)
|
||||||
//TODO: only use this on supported targets
|
.standard_filters(false)
|
||||||
.same_file_system(true)
|
.hidden(!show_hidden)
|
||||||
.build_parallel()
|
//TODO: only use this on supported targets
|
||||||
.run(|| {
|
.same_file_system(true)
|
||||||
Box::new(|entry_res| {
|
.build_parallel()
|
||||||
let Ok(entry) = entry_res else {
|
.run(|| {
|
||||||
// Skip invalid entries
|
Box::new(|entry_res| {
|
||||||
return ignore::WalkState::Skip;
|
let Ok(entry) = entry_res else {
|
||||||
};
|
// Skip invalid entries
|
||||||
|
return ignore::WalkState::Skip;
|
||||||
|
};
|
||||||
|
|
||||||
let Some(file_name) = entry.file_name().to_str() else {
|
let Some(file_name) = entry.file_name().to_str() else {
|
||||||
// Skip anything with an invalid name
|
// Skip anything with an invalid name
|
||||||
return ignore::WalkState::Skip;
|
return ignore::WalkState::Skip;
|
||||||
};
|
};
|
||||||
|
|
||||||
if regex.is_match(file_name) {
|
if regex.is_match(file_name) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
let metadata = match entry.metadata() {
|
let metadata = match entry.metadata() {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
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
|
||||||
);
|
);
|
||||||
return ignore::WalkState::Continue;
|
return ignore::WalkState::Continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !callback(SearchItem::Path(
|
||||||
|
path.to_path_buf(),
|
||||||
|
file_name.to_string(),
|
||||||
|
metadata,
|
||||||
|
)) {
|
||||||
|
return ignore::WalkState::Quit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
//TODO: use entry.into_path?
|
ignore::WalkState::Continue
|
||||||
if !callback(path, file_name, metadata) {
|
})
|
||||||
return ignore::WalkState::Quit;
|
});
|
||||||
|
}
|
||||||
|
SearchLocation::Recents => {
|
||||||
|
let recent_files = match recently_used_xbel::parse_file() {
|
||||||
|
Ok(recent_files) => recent_files,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("Error reading recent files: {err:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for bookmark in recent_files.bookmarks {
|
||||||
|
let path = uri_to_path(bookmark.href);
|
||||||
|
if let Some(path) = path
|
||||||
|
&& path.exists()
|
||||||
|
{
|
||||||
|
let file_name = path.file_name();
|
||||||
|
if let Some(file_name) = file_name {
|
||||||
|
let file_name = file_name.to_string_lossy();
|
||||||
|
if regex.is_match(&file_name) {
|
||||||
|
match path.metadata() {
|
||||||
|
Ok(metadata) => {
|
||||||
|
if !callback(SearchItem::Path(
|
||||||
|
path.to_path_buf(),
|
||||||
|
file_name.to_string(),
|
||||||
|
metadata,
|
||||||
|
)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to read metadata for entry at {}: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ignore::WalkState::Continue
|
}
|
||||||
})
|
SearchLocation::Trash => {
|
||||||
});
|
trash_helpers::scan_search_trash(callback, ®ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This config statement is from trash::os_limited, inverted
|
// This config statement is from trash::os_limited, inverted
|
||||||
|
|
@ -1123,9 +1202,31 @@ pub fn scan_search<F: Fn(&Path, &str, Metadata) -> bool + Sync>(
|
||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
)
|
)
|
||||||
)))]
|
)))]
|
||||||
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
|
mod trash_helpers {
|
||||||
log::warn!("viewing trash not supported on this platform");
|
use super::*;
|
||||||
Vec::new()
|
|
||||||
|
pub fn trash_entries() -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
||||||
|
widget::icon::from_name("user-trash")
|
||||||
|
.size(icon_size)
|
||||||
|
.handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||||
|
widget::icon::from_name("user-trash-symbolic")
|
||||||
|
.size(icon_size)
|
||||||
|
.handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
|
||||||
|
log::warn!("viewing trash not supported on this platform");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This config statement is from trash::os_limited
|
// This config statement is from trash::os_limited
|
||||||
|
|
@ -1138,76 +1239,85 @@ pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
|
||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
pub mod trash_helpers {
|
||||||
let entries = match trash::os_limited::list() {
|
use super::*;
|
||||||
Ok(entry) => entry,
|
|
||||||
Err(err) => {
|
pub fn trash_entries() -> usize {
|
||||||
log::warn!("failed to read trash items: {err}");
|
match trash::os_limited::list() {
|
||||||
return Vec::new();
|
Ok(entries) => entries.len(),
|
||||||
|
Err(_err) => 0,
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
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 name = entry.name.to_string_lossy().into_owned();
|
|
||||||
let display_name = Item::display_name(&name);
|
|
||||||
|
|
||||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) =
|
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
||||||
match metadata.size {
|
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
||||||
trash::TrashItemSize::Entries(_) => (
|
"user-trash"
|
||||||
//TODO: make this a static
|
} else {
|
||||||
"inode/directory".parse().unwrap(),
|
"user-trash-full"
|
||||||
folder_icon(&original_path, sizes.grid()),
|
|
||||||
folder_icon(&original_path, sizes.list()),
|
|
||||||
folder_icon(&original_path, sizes.list_condensed()),
|
|
||||||
),
|
|
||||||
trash::TrashItemSize::Bytes(_) => {
|
|
||||||
// This passes remote = true so it does not read from the original path
|
|
||||||
let mime = mime_for_path(&original_path, None, true);
|
|
||||||
(
|
|
||||||
mime.clone(),
|
|
||||||
mime_icon(mime.clone(), sizes.grid()),
|
|
||||||
mime_icon(mime.clone(), sizes.list()),
|
|
||||||
mime_icon(mime, sizes.list_condensed()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(Item {
|
|
||||||
name,
|
|
||||||
display_name,
|
|
||||||
is_mount_point: false,
|
|
||||||
metadata: ItemMetadata::Trash { metadata, entry },
|
|
||||||
hidden: false,
|
|
||||||
location_opt: None,
|
|
||||||
mime,
|
|
||||||
icon_handle_grid,
|
|
||||||
icon_handle_list,
|
|
||||||
icon_handle_list_condensed,
|
|
||||||
thumbnail_opt: Some(ItemThumbnail::NotImage),
|
|
||||||
button_id: widget::Id::unique(),
|
|
||||||
pos_opt: Cell::new(None),
|
|
||||||
rect_opt: Cell::new(None),
|
|
||||||
selected: false,
|
|
||||||
highlighted: false,
|
|
||||||
overlaps_drag_rect: false,
|
|
||||||
dir_size: DirSize::NotDirectory,
|
|
||||||
cut: false,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
.size(icon_size)
|
||||||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
.handle()
|
||||||
(true, false) => Ordering::Less,
|
}
|
||||||
(false, true) => Ordering::Greater,
|
|
||||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||||
});
|
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
||||||
items
|
"user-trash-symbolic"
|
||||||
|
} else {
|
||||||
|
"user-trash-full-symbolic"
|
||||||
|
})
|
||||||
|
.size(icon_size)
|
||||||
|
.handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||||
|
let entries = match trash::os_limited::list() {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read trash items: {err}");
|
||||||
|
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()?;
|
||||||
|
Some(item_from_trash_entry(entry, metadata, sizes))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||||
|
(true, false) => Ordering::Less,
|
||||||
|
(false, true) => Ordering::Greater,
|
||||||
|
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||||
|
});
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||||
|
let entries = match trash::os_limited::list() {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read trash items: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
|
||||||
|
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||||
|
}) {
|
||||||
|
let name = entry.name.to_string_lossy();
|
||||||
|
if regex.is_match(&name) {
|
||||||
|
if !callback(SearchItem::Trash(entry, metadata)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uri_to_path(uri: String) -> Option<PathBuf> {
|
fn uri_to_path(uri: String) -> Option<PathBuf> {
|
||||||
|
|
@ -1263,7 +1373,7 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
||||||
let item = item_from_entry(path, name, metadata, sizes);
|
let item = item_from_entry(path, name, metadata, sizes);
|
||||||
Some((item, last_edit.min(last_visit)))
|
Some((item, last_edit.min(last_visit)))
|
||||||
} else {
|
} else {
|
||||||
log::warn!("recent file path not exist: {}", path.display());
|
log::warn!("recent file path does not exist: {}", path.display());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -1343,15 +1453,15 @@ pub fn scan_desktop(
|
||||||
let display_name = Item::display_name(&name);
|
let display_name = Item::display_name(&name);
|
||||||
|
|
||||||
let metadata = ItemMetadata::SimpleDir {
|
let metadata = ItemMetadata::SimpleDir {
|
||||||
entries: trash_entries() as u64,
|
entries: trash_helpers::trash_entries() as u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||||
(
|
(
|
||||||
"inode/directory".parse().unwrap(),
|
"inode/directory".parse().unwrap(),
|
||||||
trash_icon(sizes.grid()),
|
trash_helpers::trash_icon(sizes.grid()),
|
||||||
trash_icon(sizes.list()),
|
trash_helpers::trash_icon(sizes.list()),
|
||||||
trash_icon(sizes.list_condensed()),
|
trash_helpers::trash_icon(sizes.list_condensed()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1436,6 +1546,29 @@ impl EditLocation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum SearchLocation {
|
||||||
|
Path(PathBuf),
|
||||||
|
Recents,
|
||||||
|
Trash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SearchLocation {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Path(path) => write!(f, "{}", path.display()),
|
||||||
|
Self::Recents => write!(f, "recents"),
|
||||||
|
Self::Trash => write!(f, "trash"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SearchItem {
|
||||||
|
Path(PathBuf, String, fs::Metadata),
|
||||||
|
Trash(TrashItem, TrashItemMetadata),
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Location> for EditLocation {
|
impl From<Location> for EditLocation {
|
||||||
fn from(location: Location) -> Self {
|
fn from(location: Location) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -1452,7 +1585,7 @@ pub enum Location {
|
||||||
Network(String, String, Option<PathBuf>),
|
Network(String, String, Option<PathBuf>),
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
Recents,
|
Recents,
|
||||||
Search(PathBuf, String, bool, Instant),
|
Search(SearchLocation, String, bool, Instant),
|
||||||
Trash,
|
Trash,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1465,7 +1598,9 @@ impl std::fmt::Display for Location {
|
||||||
Self::Network(uri, ..) => write!(f, "{uri}"),
|
Self::Network(uri, ..) => write!(f, "{uri}"),
|
||||||
Self::Path(path) => write!(f, "{}", path.display()),
|
Self::Path(path) => write!(f, "{}", path.display()),
|
||||||
Self::Recents => write!(f, "recents"),
|
Self::Recents => write!(f, "recents"),
|
||||||
Self::Search(path, term, ..) => write!(f, "search {} for {}", path.display(), term),
|
Self::Search(location, term, ..) => {
|
||||||
|
write!(f, "search {} for {}", location, term)
|
||||||
|
}
|
||||||
Self::Trash => write!(f, "trash"),
|
Self::Trash => write!(f, "trash"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1514,7 +1649,7 @@ impl Location {
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(path, ..) => Some(path),
|
Self::Desktop(path, ..) => Some(path),
|
||||||
Self::Path(path) => Some(path),
|
Self::Path(path) => Some(path),
|
||||||
Self::Search(path, ..) => Some(path),
|
Self::Search(SearchLocation::Path(path), ..) => Some(path),
|
||||||
Self::Network(_, _, path) => path.as_ref(),
|
Self::Network(_, _, path) => path.as_ref(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
@ -1524,7 +1659,7 @@ impl Location {
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(path, ..) => Some(path),
|
Self::Desktop(path, ..) => Some(path),
|
||||||
Self::Path(path) => Some(path),
|
Self::Path(path) => Some(path),
|
||||||
Self::Search(path, ..) => Some(path),
|
Self::Search(SearchLocation::Path(path), ..) => Some(path),
|
||||||
Self::Network(_, _, path) => path,
|
Self::Network(_, _, path) => path,
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
@ -1537,9 +1672,12 @@ impl Location {
|
||||||
Self::Desktop(path, display.clone(), *desktop_config)
|
Self::Desktop(path, display.clone(), *desktop_config)
|
||||||
}
|
}
|
||||||
Self::Path(..) => Self::Path(path),
|
Self::Path(..) => Self::Path(path),
|
||||||
Self::Search(_, term, show_hidden, time) => {
|
Self::Search(SearchLocation::Path(_), term, show_hidden, time) => Self::Search(
|
||||||
Self::Search(path, term.clone(), *show_hidden, *time)
|
SearchLocation::Path(path),
|
||||||
}
|
term.clone(),
|
||||||
|
*show_hidden,
|
||||||
|
*time,
|
||||||
|
),
|
||||||
|
|
||||||
other => other.clone(),
|
other => other.clone(),
|
||||||
}
|
}
|
||||||
|
|
@ -1563,7 +1701,7 @@ impl Location {
|
||||||
// Search is done incrementally
|
// Search is done incrementally
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
Self::Trash => scan_trash(sizes),
|
Self::Trash => trash_helpers::scan_trash(sizes),
|
||||||
Self::Recents => scan_recents(sizes),
|
Self::Recents => scan_recents(sizes),
|
||||||
Self::Network(uri, _, _) => scan_network(uri, sizes),
|
Self::Network(uri, _, _) => scan_network(uri, sizes),
|
||||||
};
|
};
|
||||||
|
|
@ -1591,9 +1729,14 @@ impl Location {
|
||||||
let (name, _) = folder_name(path);
|
let (name, _) = folder_name(path);
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
Self::Search(path, term, ..) => {
|
Self::Search(location, term, ..) => {
|
||||||
|
let name = match location {
|
||||||
|
SearchLocation::Path(path) => folder_name(path).0,
|
||||||
|
SearchLocation::Trash => fl!("trash"),
|
||||||
|
SearchLocation::Recents => fl!("recents"),
|
||||||
|
};
|
||||||
|
|
||||||
//TODO: translate
|
//TODO: translate
|
||||||
let (name, _) = folder_name(path);
|
|
||||||
format!("Search \"{term}\": {name}")
|
format!("Search \"{term}\": {name}")
|
||||||
}
|
}
|
||||||
Self::Trash => {
|
Self::Trash => {
|
||||||
|
|
@ -1622,6 +1765,20 @@ impl Location {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_trash(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Location::Trash | Location::Search(SearchLocation::Trash, ..)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_recents(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Location::Recents | Location::Search(SearchLocation::Recents, ..)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if this location supports paste operations (not Trash)
|
/// Returns true if this location supports paste operations (not Trash)
|
||||||
pub fn supports_paste(&self) -> bool {
|
pub fn supports_paste(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
|
|
@ -2241,9 +2398,9 @@ impl Item {
|
||||||
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||||
widget::text::body(name)
|
widget::text::body(name)
|
||||||
.wrapping(text::Wrapping::WordOrGlyph)
|
.wrapping(text::Wrapping::WordOrGlyph)
|
||||||
.ellipsize(text::Ellipsize::Middle(
|
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
|
||||||
text::EllipsizeHeightLimit::Lines(3),
|
3,
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text widget for a filename in list view: word-or-glyph wrapping, middle-ellipsized to 1 line.
|
/// Text widget for a filename in list view: word-or-glyph wrapping, middle-ellipsized to 1 line.
|
||||||
|
|
@ -2252,9 +2409,9 @@ impl Item {
|
||||||
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||||
widget::text::body(name)
|
widget::text::body(name)
|
||||||
.wrapping(text::Wrapping::WordOrGlyph)
|
.wrapping(text::Wrapping::WordOrGlyph)
|
||||||
.ellipsize(text::Ellipsize::Middle(
|
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
|
||||||
text::EllipsizeHeightLimit::Lines(1),
|
1,
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_opt(&self) -> Option<&PathBuf> {
|
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
|
|
@ -2614,7 +2771,7 @@ impl Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchContext {
|
struct SearchContext {
|
||||||
results_rx: mpsc::Receiver<(PathBuf, String, Metadata)>,
|
results_rx: mpsc::Receiver<SearchItem>,
|
||||||
ready: Arc<atomic::AtomicBool>,
|
ready: Arc<atomic::AtomicBool>,
|
||||||
last_modified_opt: Arc<RwLock<Option<SystemTime>>>,
|
last_modified_opt: Arc<RwLock<Option<SystemTime>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -4080,21 +4237,26 @@ impl Tab {
|
||||||
if let Some(items) = &mut self.items_opt {
|
if let Some(items) = &mut self.items_opt {
|
||||||
if finished || context.ready.swap(false, atomic::Ordering::SeqCst) {
|
if finished || context.ready.swap(false, atomic::Ordering::SeqCst) {
|
||||||
let duration = Instant::now();
|
let duration = Instant::now();
|
||||||
while let Ok((path, name, metadata)) = context.results_rx.try_recv() {
|
while let Ok(search_item) = context.results_rx.try_recv() {
|
||||||
//TODO: combine this with column_sort logic, they must match!
|
//TODO: combine this with column_sort logic, they must match!
|
||||||
let item_modified = metadata.modified().ok();
|
let index =
|
||||||
let index = match items.binary_search_by(|other| {
|
if let SearchItem::Path(_, _, ref metadata) = search_item {
|
||||||
item_modified.cmp(&other.metadata.modified())
|
let item_modified = metadata.modified().ok();
|
||||||
}) {
|
match items.binary_search_by(|other| {
|
||||||
Ok(index) => index,
|
item_modified.cmp(&other.metadata.modified())
|
||||||
Err(index) => index,
|
}) {
|
||||||
};
|
Ok(index) => index,
|
||||||
|
Err(index) => index,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.len()
|
||||||
|
};
|
||||||
|
|
||||||
if index < MAX_SEARCH_RESULTS {
|
if index < MAX_SEARCH_RESULTS {
|
||||||
//TODO: use correct IconSizes
|
//TODO: use correct IconSizes
|
||||||
items.insert(
|
let item =
|
||||||
index,
|
item_from_search_item(search_item, IconSizes::default());
|
||||||
item_from_entry(path, name, metadata, IconSizes::default()),
|
items.insert(index, item);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Ensure that updates make it to the GUI in a timely manner
|
// Ensure that updates make it to the GUI in a timely manner
|
||||||
if !finished && duration.elapsed() >= MAX_SEARCH_LATENCY {
|
if !finished && duration.elapsed() >= MAX_SEARCH_LATENCY {
|
||||||
|
|
@ -4822,7 +4984,7 @@ impl Tab {
|
||||||
|
|
||||||
let heading_row = widget::row::with_children([
|
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.is_trash() {
|
||||||
heading_item(
|
heading_item(
|
||||||
fl!("trashed-on"),
|
fl!("trashed-on"),
|
||||||
Length::Fixed(modified_width),
|
Length::Fixed(modified_width),
|
||||||
|
|
@ -4946,7 +5108,9 @@ impl Tab {
|
||||||
|
|
||||||
let mut children: Vec<Element<_>> = Vec::new();
|
let mut children: Vec<Element<_>> = Vec::new();
|
||||||
match &self.location {
|
match &self.location {
|
||||||
Location::Desktop(path, ..) | Location::Path(path) | Location::Search(path, ..) => {
|
Location::Desktop(path, ..)
|
||||||
|
| Location::Path(path)
|
||||||
|
| Location::Search(SearchLocation::Path(path), ..) => {
|
||||||
let excess_str = "...";
|
let excess_str = "...";
|
||||||
let excess_width = text_width_body(excess_str);
|
let excess_width = text_width_body(excess_str);
|
||||||
for (index, ancestor) in path.ancestors().enumerate() {
|
for (index, ancestor) in path.ancestors().enumerate() {
|
||||||
|
|
@ -5033,7 +5197,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
children.reverse();
|
children.reverse();
|
||||||
}
|
}
|
||||||
Location::Trash => {
|
Location::Trash | Location::Search(SearchLocation::Trash, ..) => {
|
||||||
children.push(
|
children.push(
|
||||||
widget::button::custom(widget::text::heading(fl!("trash")))
|
widget::button::custom(widget::text::heading(fl!("trash")))
|
||||||
.padding(space_xxxs)
|
.padding(space_xxxs)
|
||||||
|
|
@ -5042,7 +5206,7 @@ impl Tab {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Location::Recents => {
|
Location::Recents | Location::Search(SearchLocation::Recents, ..) => {
|
||||||
children.push(
|
children.push(
|
||||||
widget::button::custom(widget::text::heading(fl!("recents")))
|
widget::button::custom(widget::text::heading(fl!("recents")))
|
||||||
.padding(space_xxxs)
|
.padding(space_xxxs)
|
||||||
|
|
@ -5422,9 +5586,9 @@ impl Tab {
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
widget::button::custom(
|
widget::button::custom(Item::grid_display_name(
|
||||||
Item::grid_display_name(item.display_name.clone()),
|
item.display_name.clone(),
|
||||||
)
|
))
|
||||||
.id(item.button_id.clone())
|
.id(item.button_id.clone())
|
||||||
.on_press(Message::Click(Some(*i)))
|
.on_press(Message::Click(Some(*i)))
|
||||||
.padding([0, space_xxxs])
|
.padding([0, space_xxxs])
|
||||||
|
|
@ -5978,8 +6142,7 @@ impl Tab {
|
||||||
tab_column = tab_column.push(popover);
|
tab_column = tab_column.push(popover);
|
||||||
}
|
}
|
||||||
match &self.location {
|
match &self.location {
|
||||||
Location::Recents => {}
|
Location::Trash | Location::Search(SearchLocation::Trash, ..) => {
|
||||||
Location::Trash => {
|
|
||||||
if let Some(items) = self.items_opt()
|
if let Some(items) = self.items_opt()
|
||||||
&& !items.is_empty()
|
&& !items.is_empty()
|
||||||
{
|
{
|
||||||
|
|
@ -6358,9 +6521,9 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load search items incrementally
|
// Load search items incrementally
|
||||||
if let Location::Search(path, term, show_hidden, start) = &self.location {
|
if let Location::Search(search_location, term, show_hidden, start) = &self.location {
|
||||||
let location = self.location.clone();
|
let location = self.location.clone();
|
||||||
let path = path.clone();
|
let search_location = search_location.clone();
|
||||||
let term = term.clone();
|
let term = term.clone();
|
||||||
let show_hidden = *show_hidden;
|
let show_hidden = *show_hidden;
|
||||||
let start = *start;
|
let start = *start;
|
||||||
|
|
@ -6389,27 +6552,25 @@ impl Tab {
|
||||||
let output = output.clone();
|
let output = output.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
scan_search(
|
scan_search(
|
||||||
&path,
|
&search_location,
|
||||||
&term,
|
&term,
|
||||||
show_hidden,
|
show_hidden,
|
||||||
move |path, name, metadata| -> bool {
|
move |search_item| -> bool {
|
||||||
// Don't send if the result is too old
|
// Don't send if the result is too old
|
||||||
if let Some(last_modified) = *last_modified_opt.read().unwrap()
|
if let Some(last_modified) = *last_modified_opt.read().unwrap()
|
||||||
{
|
{
|
||||||
if let Ok(modified) = metadata.modified() {
|
if let SearchItem::Path(_, _, ref metadata) = search_item {
|
||||||
if modified < last_modified {
|
if let Ok(modified) = metadata.modified() {
|
||||||
|
if modified < last_modified {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match results_tx.blocking_send((
|
match results_tx.blocking_send(search_item) {
|
||||||
path.to_path_buf(),
|
|
||||||
name.to_string(),
|
|
||||||
metadata,
|
|
||||||
)) {
|
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if ready.swap(true, atomic::Ordering::SeqCst) {
|
if ready.swap(true, atomic::Ordering::SeqCst) {
|
||||||
true
|
true
|
||||||
|
|
@ -6432,7 +6593,7 @@ impl Tab {
|
||||||
log::info!(
|
log::info!(
|
||||||
"searched for {:?} in {} in {:?}",
|
"searched for {:?} in {} in {:?}",
|
||||||
term,
|
term,
|
||||||
path.display(),
|
search_location,
|
||||||
start.elapsed(),
|
start.elapsed(),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue