parent
c7511bbbe6
commit
e25cd37f2d
7 changed files with 414 additions and 410 deletions
146
src/app.rs
146
src/app.rs
|
|
@ -59,13 +59,11 @@ use wayland_client::{protocol::wl_output::WlOutput, Proxy};
|
||||||
use crate::{
|
use crate::{
|
||||||
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
|
||||||
config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig},
|
config::{AppTheme, Config, DesktopConfig, Favorite, IconSizes, TabConfig},
|
||||||
desktop_dir, fl, home_dir,
|
fl, home_dir,
|
||||||
key_bind::key_binds,
|
key_bind::key_binds,
|
||||||
localize::LANGUAGE_SORTER,
|
localize::LANGUAGE_SORTER,
|
||||||
menu, mime_app, mime_icon,
|
menu, mime_app, mime_icon,
|
||||||
mounter::{
|
mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
|
||||||
mounters, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters,
|
|
||||||
},
|
|
||||||
operation::{Operation, ReplaceResult},
|
operation::{Operation, ReplaceResult},
|
||||||
spawn_detached::spawn_detached,
|
spawn_detached::spawn_detached,
|
||||||
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION},
|
||||||
|
|
@ -308,7 +306,6 @@ pub enum Message {
|
||||||
SearchActivate,
|
SearchActivate,
|
||||||
SearchClear,
|
SearchClear,
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SearchSubmit,
|
|
||||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||||
TabActivate(Entity),
|
TabActivate(Entity),
|
||||||
TabNext,
|
TabNext,
|
||||||
|
|
@ -491,7 +488,6 @@ pub struct App {
|
||||||
dialog_text_input: widget::Id,
|
dialog_text_input: widget::Id,
|
||||||
key_binds: HashMap<KeyBind, Action>,
|
key_binds: HashMap<KeyBind, Action>,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
mounters: Mounters,
|
|
||||||
mounter_items: HashMap<MounterKey, MounterItems>,
|
mounter_items: HashMap<MounterKey, MounterItems>,
|
||||||
network_drive_connecting: Option<(MounterKey, String)>,
|
network_drive_connecting: Option<(MounterKey, String)>,
|
||||||
network_drive_input: String,
|
network_drive_input: String,
|
||||||
|
|
@ -501,9 +497,7 @@ pub struct App {
|
||||||
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
||||||
complete_operations: BTreeMap<u64, Operation>,
|
complete_operations: BTreeMap<u64, Operation>,
|
||||||
failed_operations: BTreeMap<u64, (Operation, String)>,
|
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||||
search_active: bool,
|
|
||||||
search_id: widget::Id,
|
search_id: widget::Id,
|
||||||
search_input: String,
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
surface_ids: HashMap<WlOutput, WindowId>,
|
surface_ids: HashMap<WlOutput, WindowId>,
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
|
|
@ -588,17 +582,11 @@ impl App {
|
||||||
selection_path: Option<PathBuf>,
|
selection_path: Option<PathBuf>,
|
||||||
) -> Command<Message> {
|
) -> Command<Message> {
|
||||||
log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}");
|
log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}");
|
||||||
let desktop_config = self.config.desktop;
|
|
||||||
let mounters = self.mounters.clone();
|
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
let location2 = location.clone();
|
let location2 = location.clone();
|
||||||
match tokio::task::spawn_blocking(move || {
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||||
location2.scan(desktop_config, mounters, icon_sizes)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
message::app(Message::TabRescan(entity, location, items, selection_path))
|
message::app(Message::TabRescan(entity, location, items, selection_path))
|
||||||
}
|
}
|
||||||
|
|
@ -630,25 +618,55 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search(&mut self) -> Command<Message> {
|
fn search(&mut self) -> Command<Message> {
|
||||||
|
if let Some(term) = self.search_get() {
|
||||||
|
self.search_set(Some(term.to_string()))
|
||||||
|
} else {
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_get(&self) -> Option<&str> {
|
||||||
|
let entity = self.tab_model.active();
|
||||||
|
let tab = self.tab_model.data::<Tab>(entity)?;
|
||||||
|
match &tab.location {
|
||||||
|
Location::Search(_, term, ..) => Some(term),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_set(&mut self, term_opt: Option<String>) -> Command<Message> {
|
||||||
let entity = self.tab_model.active();
|
let entity = self.tab_model.active();
|
||||||
let mut title_location_opt = None;
|
let mut title_location_opt = None;
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if let Some(path) = tab.location.path_opt() {
|
let location_opt = match term_opt {
|
||||||
let location = if !self.search_input.is_empty() {
|
Some(term) => match &tab.location {
|
||||||
Location::Search(path.to_path_buf(), self.search_input.clone())
|
Location::Path(path) | Location::Search(path, ..) => Some((
|
||||||
} else {
|
Location::Search(path.to_path_buf(), term, Instant::now()),
|
||||||
Location::Path(path.to_path_buf())
|
true,
|
||||||
};
|
)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
None => match &tab.location {
|
||||||
|
Location::Search(path, ..) => Some((Location::Path(path.to_path_buf()), false)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Some((location, focus_search)) = location_opt {
|
||||||
tab.change_location(&location, None);
|
tab.change_location(&location, None);
|
||||||
title_location_opt = Some((tab.title(), tab.location.clone()));
|
title_location_opt = Some((tab.title(), tab.location.clone(), focus_search));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((title, location)) = title_location_opt {
|
if let Some((title, location, focus_search)) = title_location_opt {
|
||||||
self.tab_model.text_set(entity, title);
|
self.tab_model.text_set(entity, title);
|
||||||
return Command::batch([
|
return Command::batch([
|
||||||
self.update_title(),
|
self.update_title(),
|
||||||
self.update_watcher(),
|
self.update_watcher(),
|
||||||
self.rescan_tab(entity, location, None),
|
self.rescan_tab(entity, location, None),
|
||||||
|
if focus_search {
|
||||||
|
widget::text_input::focus(self.search_id.clone())
|
||||||
|
} else {
|
||||||
|
Command::none()
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
Command::none()
|
Command::none()
|
||||||
|
|
@ -760,7 +778,7 @@ impl App {
|
||||||
.divider_above()
|
.divider_above()
|
||||||
});
|
});
|
||||||
|
|
||||||
if !self.mounters.is_empty() {
|
if !MOUNTERS.is_empty() {
|
||||||
nav_model = nav_model.insert(|b| {
|
nav_model = nav_model.insert(|b| {
|
||||||
b.text(fl!("networks"))
|
b.text(fl!("networks"))
|
||||||
.icon(widget::icon::icon(
|
.icon(widget::icon::icon(
|
||||||
|
|
@ -1226,7 +1244,6 @@ impl Application for App {
|
||||||
dialog_text_input: widget::Id::unique(),
|
dialog_text_input: widget::Id::unique(),
|
||||||
key_binds,
|
key_binds,
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
mounters: mounters(),
|
|
||||||
mounter_items: HashMap::new(),
|
mounter_items: HashMap::new(),
|
||||||
network_drive_connecting: None,
|
network_drive_connecting: None,
|
||||||
network_drive_input: String::new(),
|
network_drive_input: String::new(),
|
||||||
|
|
@ -1236,9 +1253,7 @@ impl Application for App {
|
||||||
pending_operations: BTreeMap::new(),
|
pending_operations: BTreeMap::new(),
|
||||||
complete_operations: BTreeMap::new(),
|
complete_operations: BTreeMap::new(),
|
||||||
failed_operations: BTreeMap::new(),
|
failed_operations: BTreeMap::new(),
|
||||||
search_active: false,
|
|
||||||
search_id: widget::Id::unique(),
|
search_id: widget::Id::unique(),
|
||||||
search_input: String::new(),
|
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
surface_ids: HashMap::new(),
|
surface_ids: HashMap::new(),
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
|
|
@ -1364,9 +1379,6 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_nav_select(&mut self, entity: Entity) -> Command<Self::Message> {
|
fn on_nav_select(&mut self, entity: Entity) -> Command<Self::Message> {
|
||||||
self.search_active = false;
|
|
||||||
self.search_input.clear();
|
|
||||||
|
|
||||||
self.nav_model.activate(entity);
|
self.nav_model.activate(entity);
|
||||||
if let Some(location) = self.nav_model.data::<Location>(entity) {
|
if let Some(location) = self.nav_model.data::<Location>(entity) {
|
||||||
let message = Message::TabMessage(None, tab::Message::Location(location.clone()));
|
let message = Message::TabMessage(None, tab::Message::Location(location.clone()));
|
||||||
|
|
@ -1374,7 +1386,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() {
|
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() {
|
||||||
if let Some(mounter) = self.mounters.get(&data.0) {
|
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
||||||
return mounter.mount(data.1.clone()).map(|_| message::none());
|
return mounter.mount(data.1.clone()).map(|_| message::none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1391,7 +1403,7 @@ impl Application for App {
|
||||||
|
|
||||||
fn on_context_drawer(&mut self) -> Command<Self::Message> {
|
fn on_context_drawer(&mut self) -> Command<Self::Message> {
|
||||||
match self.context_page {
|
match self.context_page {
|
||||||
ContextPage::Preview(_, _) => {
|
ContextPage::Preview(..) => {
|
||||||
// Persist state of preview page
|
// Persist state of preview page
|
||||||
if self.core.window.show_context != self.config.show_details {
|
if self.core.window.show_context != self.config.show_details {
|
||||||
return self.update(Message::Preview(None));
|
return self.update(Message::Preview(None));
|
||||||
|
|
@ -1426,10 +1438,9 @@ impl Application for App {
|
||||||
self.set_show_context(false);
|
self.set_show_context(false);
|
||||||
return Command::none();
|
return Command::none();
|
||||||
}
|
}
|
||||||
if self.search_active {
|
if self.search_get().is_some() {
|
||||||
// Close search if open
|
// Close search if open
|
||||||
self.search_active = false;
|
return self.search_set(None);
|
||||||
return Command::none();
|
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||||
if tab.context_menu.is_some() {
|
if tab.context_menu.is_some() {
|
||||||
|
|
@ -1815,7 +1826,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::NetworkDriveSubmit => {
|
Message::NetworkDriveSubmit => {
|
||||||
//TODO: know which mounter to use for network drives
|
//TODO: know which mounter to use for network drives
|
||||||
for (mounter_key, mounter) in self.mounters.iter() {
|
for (mounter_key, mounter) in MOUNTERS.iter() {
|
||||||
self.network_drive_connecting =
|
self.network_drive_connecting =
|
||||||
Some((*mounter_key, self.network_drive_input.clone()));
|
Some((*mounter_key, self.network_drive_input.clone()));
|
||||||
return mounter
|
return mounter
|
||||||
|
|
@ -2171,11 +2182,8 @@ impl Application for App {
|
||||||
commands.push(self.update_notification());
|
commands.push(self.update_notification());
|
||||||
// Manually rescan any trash tabs after any operation is completed
|
// Manually rescan any trash tabs after any operation is completed
|
||||||
commands.push(self.rescan_trash());
|
commands.push(self.rescan_trash());
|
||||||
|
|
||||||
// if search is active, update "search" tab view
|
// if search is active, update "search" tab view
|
||||||
if !self.search_input.is_empty() {
|
commands.push(self.search());
|
||||||
commands.push(self.search());
|
|
||||||
}
|
|
||||||
return Command::batch(commands);
|
return Command::batch(commands);
|
||||||
}
|
}
|
||||||
Message::PendingError(id, err) => {
|
Message::PendingError(id, err) => {
|
||||||
|
|
@ -2327,33 +2335,17 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SearchActivate => {
|
Message::SearchActivate => {
|
||||||
self.search_active = true;
|
return if self.search_get().is_none() {
|
||||||
return widget::text_input::focus(self.search_id.clone());
|
self.search_set(Some(String::new()))
|
||||||
|
} else {
|
||||||
|
widget::text_input::focus(self.search_id.clone())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Message::SearchClear => {
|
Message::SearchClear => {
|
||||||
self.search_active = false;
|
return self.search_set(None);
|
||||||
self.search_input.clear();
|
|
||||||
}
|
}
|
||||||
Message::SearchInput(input) => {
|
Message::SearchInput(input) => {
|
||||||
if input != self.search_input {
|
return self.search_set(Some(input));
|
||||||
self.search_input = input;
|
|
||||||
/*TODO: live search? (probably needs subscription for streaming results)
|
|
||||||
// This performs live search
|
|
||||||
if !self.search_input.is_empty() {
|
|
||||||
return self.search();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::SearchSubmit => {
|
|
||||||
if !self.search_input.is_empty() {
|
|
||||||
return self.search();
|
|
||||||
} else {
|
|
||||||
// rescan the tab to get the contents back
|
|
||||||
// and exit search
|
|
||||||
self.search_active = false;
|
|
||||||
return self.search();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Message::SystemThemeModeChange(_theme_mode) => {
|
Message::SystemThemeModeChange(_theme_mode) => {
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
|
|
@ -2364,14 +2356,7 @@ impl Application for App {
|
||||||
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
|
||||||
self.activate_nav_model_location(&tab.location.clone());
|
self.activate_nav_model_location(&tab.location.clone());
|
||||||
}
|
}
|
||||||
let mut commands = vec![];
|
return self.update_title();
|
||||||
commands.push(self.update_title());
|
|
||||||
// if the tab was in an active search mode
|
|
||||||
// search again in case files were modified/deleted
|
|
||||||
if !self.search_input.is_empty() {
|
|
||||||
commands.push(self.search());
|
|
||||||
}
|
|
||||||
return Command::batch(commands);
|
|
||||||
}
|
}
|
||||||
Message::TabNext => {
|
Message::TabNext => {
|
||||||
let len = self.tab_model.iter().count();
|
let len = self.tab_model.iter().count();
|
||||||
|
|
@ -2658,15 +2643,11 @@ impl Application for App {
|
||||||
self.toasts.remove(id);
|
self.toasts.remove(id);
|
||||||
|
|
||||||
let mut paths = Vec::with_capacity(recently_trashed.len());
|
let mut paths = Vec::with_capacity(recently_trashed.len());
|
||||||
let desktop_config = self.config.desktop;
|
|
||||||
let mounters = self.mounters.clone();
|
|
||||||
let icon_sizes = self.config.tab.icon_sizes;
|
let icon_sizes = self.config.tab.icon_sizes;
|
||||||
|
|
||||||
return cosmic::command::future(async move {
|
return cosmic::command::future(async move {
|
||||||
match tokio::task::spawn_blocking(move || {
|
match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes))
|
||||||
Location::Trash.scan(desktop_config, mounters, icon_sizes)
|
.await
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
for path in &*recently_trashed {
|
for path in &*recently_trashed {
|
||||||
|
|
@ -2885,7 +2866,7 @@ impl Application for App {
|
||||||
|
|
||||||
Message::NavBarClose(entity) => {
|
Message::NavBarClose(entity) => {
|
||||||
if let Some(data) = self.nav_model.data::<MounterData>(entity) {
|
if let Some(data) = self.nav_model.data::<MounterData>(entity) {
|
||||||
if let Some(mounter) = self.mounters.get(&data.0) {
|
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
||||||
return mounter.unmount(data.1.clone()).map(|_| message::none());
|
return mounter.unmount(data.1.clone()).map(|_| message::none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3005,7 +2986,7 @@ impl Application for App {
|
||||||
};
|
};
|
||||||
|
|
||||||
let (entity, command) = self.open_tab_entity(
|
let (entity, command) = self.open_tab_entity(
|
||||||
Location::Desktop(desktop_dir(), display),
|
Location::Desktop(crate::desktop_dir(), display),
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
@ -3614,14 +3595,13 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.search_active {
|
if let Some(term) = self.search_get() {
|
||||||
elements.push(
|
elements.push(
|
||||||
widget::text_input::search_input("", &self.search_input)
|
widget::text_input::search_input("", term)
|
||||||
.width(Length::Fixed(240.0))
|
.width(Length::Fixed(240.0))
|
||||||
.id(self.search_id.clone())
|
.id(self.search_id.clone())
|
||||||
.on_clear(Message::SearchClear)
|
.on_clear(Message::SearchClear)
|
||||||
.on_input(Message::SearchInput)
|
.on_input(Message::SearchInput)
|
||||||
.on_submit(Message::SearchSubmit)
|
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3955,7 +3935,7 @@ impl Application for App {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (key, mounter) in self.mounters.iter() {
|
for (key, mounter) in MOUNTERS.iter() {
|
||||||
let key = *key;
|
let key = *key;
|
||||||
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
||||||
match mounter_message {
|
match mounter_message {
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, CosmicConfigEntry, Deserialize, Serialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct DesktopConfig {
|
pub struct DesktopConfig {
|
||||||
pub show_content: bool,
|
pub show_content: bool,
|
||||||
|
|
|
||||||
151
src/dialog.rs
151
src/dialog.rs
|
|
@ -35,7 +35,7 @@ use std::{
|
||||||
env, fmt, fs,
|
env, fmt, fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
time,
|
time::{self, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -45,7 +45,7 @@ use crate::{
|
||||||
key_bind::key_binds,
|
key_bind::key_binds,
|
||||||
localize::LANGUAGE_SORTER,
|
localize::LANGUAGE_SORTER,
|
||||||
menu,
|
menu,
|
||||||
mounter::{mounters, MounterItem, MounterItems, MounterKey, MounterMessage, Mounters},
|
mounter::{MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
|
||||||
tab::{self, ItemMetadata, Location, Tab},
|
tab::{self, ItemMetadata, Location, Tab},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -323,7 +323,6 @@ enum Message {
|
||||||
SearchActivate,
|
SearchActivate,
|
||||||
SearchClear,
|
SearchClear,
|
||||||
SearchInput(String),
|
SearchInput(String),
|
||||||
SearchSubmit,
|
|
||||||
TabMessage(tab::Message),
|
TabMessage(tab::Message),
|
||||||
TabRescan(Vec<tab::Item>),
|
TabRescan(Vec<tab::Item>),
|
||||||
}
|
}
|
||||||
|
|
@ -380,22 +379,31 @@ struct App {
|
||||||
filter_selected: Option<usize>,
|
filter_selected: Option<usize>,
|
||||||
filename_id: widget::Id,
|
filename_id: widget::Id,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
mounters: Mounters,
|
|
||||||
mounter_items: HashMap<MounterKey, MounterItems>,
|
mounter_items: HashMap<MounterKey, MounterItems>,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
result_opt: Option<DialogResult>,
|
result_opt: Option<DialogResult>,
|
||||||
search_active: bool,
|
|
||||||
search_id: widget::Id,
|
search_id: widget::Id,
|
||||||
search_input: String,
|
|
||||||
tab: Tab,
|
tab: Tab,
|
||||||
key_binds: HashMap<KeyBind, Action>,
|
key_binds: HashMap<KeyBind, Action>,
|
||||||
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn button_row(&self) -> Element<Message> {
|
fn button_view(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
|
let mut col = widget::column::with_capacity(2)
|
||||||
|
.spacing(space_xxs)
|
||||||
|
.padding(space_xxs);
|
||||||
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||||
|
col = col.push(
|
||||||
|
widget::text_input("", filename)
|
||||||
|
.id(self.filename_id.clone())
|
||||||
|
.on_input(Message::Filename)
|
||||||
|
.on_submit(Message::Save(false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut row = widget::row::with_capacity(
|
let mut row = widget::row::with_capacity(
|
||||||
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
|
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
|
||||||
)
|
)
|
||||||
|
|
@ -429,16 +437,7 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
row = row.push(widget::horizontal_space(Length::Fill));
|
||||||
row = row.push(
|
|
||||||
widget::text_input("", filename)
|
|
||||||
.id(self.filename_id.clone())
|
|
||||||
.on_input(Message::Filename)
|
|
||||||
.on_submit(Message::Save(false)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
row = row.push(widget::horizontal_space(Length::Fill));
|
|
||||||
}
|
|
||||||
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
|
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
|
||||||
row = row.push(if self.flags.kind.save() {
|
row = row.push(if self.flags.kind.save() {
|
||||||
widget::button::suggested(&self.accept_label).on_press(Message::Save(false))
|
widget::button::suggested(&self.accept_label).on_press(Message::Save(false))
|
||||||
|
|
@ -446,7 +445,9 @@ impl App {
|
||||||
widget::button::suggested(&self.accept_label).on_press(Message::Open)
|
widget::button::suggested(&self.accept_label).on_press(Message::Open)
|
||||||
});
|
});
|
||||||
|
|
||||||
row.into()
|
col = col.push(row);
|
||||||
|
|
||||||
|
col.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(&self, kind: &PreviewKind) -> Element<AppMessage> {
|
fn preview(&self, kind: &PreviewKind) -> Element<AppMessage> {
|
||||||
|
|
@ -485,16 +486,10 @@ impl App {
|
||||||
|
|
||||||
fn rescan_tab(&self) -> Command<Message> {
|
fn rescan_tab(&self) -> Command<Message> {
|
||||||
let location = self.tab.location.clone();
|
let location = self.tab.location.clone();
|
||||||
let desktop_config = self.flags.config.desktop;
|
|
||||||
let mounters = self.mounters.clone();
|
|
||||||
let icon_sizes = self.tab.config.icon_sizes;
|
let icon_sizes = self.tab.config.icon_sizes;
|
||||||
Command::perform(
|
Command::perform(
|
||||||
async move {
|
async move {
|
||||||
match tokio::task::spawn_blocking(move || {
|
match tokio::task::spawn_blocking(move || location.scan(icon_sizes)).await {
|
||||||
location.scan(desktop_config, mounters, icon_sizes)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(items) => message::app(Message::TabRescan(items)),
|
Ok(items) => message::app(Message::TabRescan(items)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to rescan: {}", err);
|
log::warn!("failed to rescan: {}", err);
|
||||||
|
|
@ -506,21 +501,43 @@ impl App {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search(&mut self) -> Command<Message> {
|
fn search_get(&self) -> Option<&str> {
|
||||||
match &self.tab.location {
|
match &self.tab.location {
|
||||||
Location::Path(path) | Location::Search(path, ..) => {
|
Location::Search(_, term, ..) => Some(term),
|
||||||
let location = if !self.search_input.is_empty() {
|
_ => None,
|
||||||
Location::Search(path.clone(), self.search_input.clone())
|
|
||||||
} else {
|
|
||||||
Location::Path(path.clone())
|
|
||||||
};
|
|
||||||
self.tab.change_location(&location, None);
|
|
||||||
Command::batch([self.update_watcher(), self.rescan_tab()])
|
|
||||||
}
|
|
||||||
_ => Command::none(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn search_set(&mut self, term_opt: Option<String>) -> Command<Message> {
|
||||||
|
let location_opt = match term_opt {
|
||||||
|
Some(term) => match &self.tab.location {
|
||||||
|
Location::Path(path) | Location::Search(path, ..) => Some((
|
||||||
|
Location::Search(path.to_path_buf(), term, Instant::now()),
|
||||||
|
true,
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
None => match &self.tab.location {
|
||||||
|
Location::Search(path, ..) => Some((Location::Path(path.to_path_buf()), false)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Some((location, focus_search)) = location_opt {
|
||||||
|
self.tab.change_location(&location, None);
|
||||||
|
return Command::batch([
|
||||||
|
self.update_title(),
|
||||||
|
self.update_watcher(),
|
||||||
|
self.rescan_tab(),
|
||||||
|
if focus_search {
|
||||||
|
widget::text_input::focus(self.search_id.clone())
|
||||||
|
} else {
|
||||||
|
Command::none()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
Command::none()
|
||||||
|
}
|
||||||
|
|
||||||
fn update_config(&mut self) -> Command<Message> {
|
fn update_config(&mut self) -> Command<Message> {
|
||||||
self.update_nav_model();
|
self.update_nav_model();
|
||||||
Command::none()
|
Command::none()
|
||||||
|
|
@ -729,13 +746,10 @@ impl Application for App {
|
||||||
filter_selected: None,
|
filter_selected: None,
|
||||||
filename_id: widget::Id::unique(),
|
filename_id: widget::Id::unique(),
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
mounters: mounters(),
|
|
||||||
mounter_items: HashMap::new(),
|
mounter_items: HashMap::new(),
|
||||||
nav_model: segmented_button::ModelBuilder::default().build(),
|
nav_model: segmented_button::ModelBuilder::default().build(),
|
||||||
result_opt: None,
|
result_opt: None,
|
||||||
search_active: false,
|
|
||||||
search_id: widget::Id::unique(),
|
search_id: widget::Id::unique(),
|
||||||
search_input: String::new(),
|
|
||||||
tab,
|
tab,
|
||||||
key_binds,
|
key_binds,
|
||||||
watcher_opt: None,
|
watcher_opt: None,
|
||||||
|
|
@ -773,7 +787,7 @@ impl Application for App {
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children(vec![
|
||||||
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_row())
|
widget::container(self.button_view())
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(theme::Container::WindowBackground)
|
.style(theme::Container::WindowBackground)
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -867,14 +881,13 @@ impl Application for App {
|
||||||
fn header_end(&self) -> Vec<Element<Message>> {
|
fn header_end(&self) -> Vec<Element<Message>> {
|
||||||
let mut elements = Vec::with_capacity(3);
|
let mut elements = Vec::with_capacity(3);
|
||||||
|
|
||||||
if self.search_active {
|
if let Some(term) = self.search_get() {
|
||||||
elements.push(
|
elements.push(
|
||||||
widget::text_input::search_input("", &self.search_input)
|
widget::text_input::search_input("", term)
|
||||||
.width(Length::Fixed(240.0))
|
.width(Length::Fixed(240.0))
|
||||||
.id(self.search_id.clone())
|
.id(self.search_id.clone())
|
||||||
.on_clear(Message::SearchClear)
|
.on_clear(Message::SearchClear)
|
||||||
.on_input(Message::SearchInput)
|
.on_input(Message::SearchInput)
|
||||||
.on_submit(Message::SearchSubmit)
|
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -942,9 +955,6 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Command<Message> {
|
fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Command<Message> {
|
||||||
self.search_active = false;
|
|
||||||
self.search_input.clear();
|
|
||||||
|
|
||||||
self.nav_model.activate(entity);
|
self.nav_model.activate(entity);
|
||||||
if let Some(location) = self.nav_model.data::<Location>(entity) {
|
if let Some(location) = self.nav_model.data::<Location>(entity) {
|
||||||
let message = Message::TabMessage(tab::Message::Location(location.clone()));
|
let message = Message::TabMessage(tab::Message::Location(location.clone()));
|
||||||
|
|
@ -952,7 +962,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() {
|
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() {
|
||||||
if let Some(mounter) = self.mounters.get(&data.0) {
|
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
||||||
return mounter.mount(data.1.clone()).map(|_| message::none());
|
return mounter.mount(data.1.clone()).map(|_| message::none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -966,10 +976,9 @@ impl Application for App {
|
||||||
return Command::none();
|
return Command::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.search_active {
|
if self.search_get().is_some() {
|
||||||
// Close search if open
|
// Close search if open
|
||||||
self.search_active = false;
|
return self.search_set(None);
|
||||||
return Command::none();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tab.context_menu.is_some() {
|
if self.tab.context_menu.is_some() {
|
||||||
|
|
@ -1251,7 +1260,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Preview => match self.context_page {
|
Message::Preview => match self.context_page {
|
||||||
ContextPage::Preview(_, _) => {
|
ContextPage::Preview(..) => {
|
||||||
self.core.window.show_context = !self.core.window.show_context;
|
self.core.window.show_context = !self.core.window.show_context;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -1283,33 +1292,17 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SearchActivate => {
|
Message::SearchActivate => {
|
||||||
self.search_active = true;
|
return if self.search_get().is_none() {
|
||||||
return widget::text_input::focus(self.search_id.clone());
|
self.search_set(Some(String::new()))
|
||||||
|
} else {
|
||||||
|
widget::text_input::focus(self.search_id.clone())
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Message::SearchClear => {
|
Message::SearchClear => {
|
||||||
self.search_active = false;
|
return self.search_set(None);
|
||||||
self.search_input.clear();
|
|
||||||
}
|
}
|
||||||
Message::SearchInput(input) => {
|
Message::SearchInput(input) => {
|
||||||
if input != self.search_input {
|
return self.search_set(Some(input));
|
||||||
self.search_input = input;
|
|
||||||
/*TODO: live search? (probably needs subscription for streaming results)
|
|
||||||
// This performs live search
|
|
||||||
if !self.search_input.is_empty() {
|
|
||||||
return self.search();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::SearchSubmit => {
|
|
||||||
if !self.search_input.is_empty() {
|
|
||||||
return self.search();
|
|
||||||
} else {
|
|
||||||
// rescan the tab to get the contents back
|
|
||||||
// and exit search
|
|
||||||
self.search_active = false;
|
|
||||||
return self.search();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Message::TabMessage(tab_message) => {
|
Message::TabMessage(tab_message) => {
|
||||||
let click_i_opt = match tab_message {
|
let click_i_opt = match tab_message {
|
||||||
|
|
@ -1438,7 +1431,11 @@ impl Application for App {
|
||||||
self.tab.set_items(items);
|
self.tab.set_items(items);
|
||||||
|
|
||||||
// Reset focus on location change
|
// Reset focus on location change
|
||||||
return widget::text_input::focus(self.filename_id.clone());
|
if self.search_get().is_some() {
|
||||||
|
return widget::text_input::focus(self.search_id.clone());
|
||||||
|
} else {
|
||||||
|
return widget::text_input::focus(self.filename_id.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1456,7 +1453,7 @@ impl Application for App {
|
||||||
.map(move |message| Message::TabMessage(message)),
|
.map(move |message| Message::TabMessage(message)),
|
||||||
);
|
);
|
||||||
|
|
||||||
tab_column = tab_column.push(self.button_row());
|
tab_column = tab_column.push(self.button_view());
|
||||||
|
|
||||||
let content: Element<_> = tab_column.into();
|
let content: Element<_> = tab_column.into();
|
||||||
|
|
||||||
|
|
@ -1566,7 +1563,7 @@ impl Application for App {
|
||||||
self.tab.subscription().map(Message::TabMessage),
|
self.tab.subscription().map(Message::TabMessage),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (key, mounter) in self.mounters.iter() {
|
for (key, mounter) in MOUNTERS.iter() {
|
||||||
let key = *key;
|
let key = *key;
|
||||||
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
subscriptions.push(mounter.subscription().map(move |mounter_message| {
|
||||||
match mounter_message {
|
match mounter_message {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// Runs application with these settings
|
/// Runs application with these settings
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/*
|
||||||
#[cfg(all(unix, not(target_os = "redox")))]
|
#[cfg(all(unix, not(target_os = "redox")))]
|
||||||
match fork::daemon(true, true) {
|
match fork::daemon(true, true) {
|
||||||
Ok(fork::Fork::Child) => (),
|
Ok(fork::Fork::Child) => (),
|
||||||
|
|
@ -91,6 +92,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||||
|
|
||||||
|
|
|
||||||
29
src/menu.rs
29
src/menu.rs
|
|
@ -58,12 +58,13 @@ pub fn context_menu<'a>(
|
||||||
.on_press(tab::Message::ContextAction(action))
|
.on_press(tab::Message::ContextAction(action))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (sort_name, sort_direction) = tab.sort_options();
|
||||||
let sort_item = |label, variant| {
|
let sort_item = |label, variant| {
|
||||||
menu_item(
|
menu_item(
|
||||||
format!(
|
format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
label,
|
label,
|
||||||
match (tab.sort_name == variant, tab.sort_direction) {
|
match (sort_name == variant, sort_direction) {
|
||||||
(true, true) => "\u{2B07}",
|
(true, true) => "\u{2B07}",
|
||||||
(true, false) => "\u{2B06}",
|
(true, false) => "\u{2B06}",
|
||||||
_ => "",
|
_ => "",
|
||||||
|
|
@ -95,10 +96,7 @@ pub fn context_menu<'a>(
|
||||||
match (&tab.mode, &tab.location) {
|
match (&tab.mode, &tab.location) {
|
||||||
(
|
(
|
||||||
tab::Mode::App | tab::Mode::Desktop,
|
tab::Mode::App | tab::Mode::Desktop,
|
||||||
Location::Desktop(_, _)
|
Location::Desktop(..) | Location::Path(..) | Location::Search(..) | Location::Recents,
|
||||||
| Location::Path(_)
|
|
||||||
| Location::Search(_, _)
|
|
||||||
| Location::Recents,
|
|
||||||
) => {
|
) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||||
|
|
@ -111,7 +109,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(_, _)) {
|
if matches!(tab.location, Location::Search(..)) {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||||
);
|
);
|
||||||
|
|
@ -200,16 +198,13 @@ pub fn context_menu<'a>(
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
tab::Mode::Dialog(dialog_kind),
|
tab::Mode::Dialog(dialog_kind),
|
||||||
Location::Desktop(_, _)
|
Location::Desktop(..) | Location::Path(..) | Location::Search(..) | Location::Recents,
|
||||||
| Location::Path(_)
|
|
||||||
| Location::Search(_, _)
|
|
||||||
| Location::Recents,
|
|
||||||
) => {
|
) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
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(_, _)) {
|
if matches!(tab.location, Location::Search(..)) {
|
||||||
children.push(
|
children.push(
|
||||||
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
|
||||||
);
|
);
|
||||||
|
|
@ -231,7 +226,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::Network(_, _)) => {
|
(_, Location::Network(..)) => {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
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());
|
||||||
|
|
@ -295,10 +290,11 @@ pub fn dialog_menu<'a>(
|
||||||
tab: &Tab,
|
tab: &Tab,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
|
let (sort_name, sort_direction) = tab.sort_options();
|
||||||
let sort_item = |label, sort, dir| {
|
let sort_item = |label, sort, dir| {
|
||||||
menu::Item::CheckBox(
|
menu::Item::CheckBox(
|
||||||
label,
|
label,
|
||||||
tab.sort_name == sort && tab.sort_direction == dir,
|
sort_name == sort && sort_direction == dir,
|
||||||
Action::SetSort(sort, dir),
|
Action::SetSort(sort, dir),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -328,7 +324,7 @@ pub fn dialog_menu<'a>(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
menu::Tree::with_children(
|
menu::Tree::with_children(
|
||||||
widget::button::icon(widget::icon::from_name(if tab.sort_direction {
|
widget::button::icon(widget::icon::from_name(if sort_direction {
|
||||||
"view-sort-ascending-symbolic"
|
"view-sort-ascending-symbolic"
|
||||||
} else {
|
} else {
|
||||||
"view-sort-descending-symbolic"
|
"view-sort-descending-symbolic"
|
||||||
|
|
@ -383,11 +379,12 @@ pub fn menu_bar<'a>(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let sort_options = tab_opt.map(|tab| tab.sort_options());
|
||||||
let sort_item = |label, sort, dir| {
|
let sort_item = |label, sort, dir| {
|
||||||
menu::Item::CheckBox(
|
menu::Item::CheckBox(
|
||||||
label,
|
label,
|
||||||
tab_opt.map_or(false, |tab| {
|
sort_options.map_or(false, |(sort_name, sort_direction)| {
|
||||||
tab.sort_name == sort && tab.sort_direction == dir
|
sort_name == sort && sort_direction == dir
|
||||||
}),
|
}),
|
||||||
Action::SetSort(sort, dir),
|
Action::SetSort(sort, dir),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use cosmic::{iced::subscription, widget, Command};
|
use cosmic::{iced::subscription, widget, Command};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
|
@ -114,3 +115,5 @@ pub fn mounters() -> Mounters {
|
||||||
|
|
||||||
Mounters::new(mounters)
|
Mounters::new(mounters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static MOUNTERS: Lazy<Mounters> = Lazy::new(|| mounters());
|
||||||
|
|
|
||||||
491
src/tab.rs
491
src/tab.rs
|
|
@ -8,6 +8,7 @@ use cosmic::{
|
||||||
alignment::{Horizontal, Vertical},
|
alignment::{Horizontal, Vertical},
|
||||||
clipboard::dnd::DndAction,
|
clipboard::dnd::DndAction,
|
||||||
event,
|
event,
|
||||||
|
futures,
|
||||||
futures::SinkExt,
|
futures::SinkExt,
|
||||||
keyboard::Modifiers,
|
keyboard::Modifiers,
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
|
|
@ -64,7 +65,7 @@ use crate::{
|
||||||
menu,
|
menu,
|
||||||
mime_app::{mime_apps, MimeApp},
|
mime_app::{mime_apps, MimeApp},
|
||||||
mime_icon::{mime_for_path, mime_icon},
|
mime_icon::{mime_for_path, mime_icon},
|
||||||
mounter::Mounters,
|
mounter::MOUNTERS,
|
||||||
mouse_area,
|
mouse_area,
|
||||||
thumbnailer::thumbnailer,
|
thumbnailer::thumbnailer,
|
||||||
};
|
};
|
||||||
|
|
@ -73,6 +74,8 @@ use uzers::{get_group_by_gid, get_user_by_uid};
|
||||||
|
|
||||||
pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
|
pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
|
||||||
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
||||||
|
//TODO: best limit for search items
|
||||||
|
const MAX_SEARCH_RESULTS: usize = 1000;
|
||||||
|
|
||||||
//TODO: adjust for locales?
|
//TODO: adjust for locales?
|
||||||
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
|
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
|
||||||
|
|
@ -525,10 +528,15 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_search<F: Fn(Item) -> bool + Sync>(
|
||||||
use rayon::prelude::ParallelSliceMut;
|
tab_path: &PathBuf,
|
||||||
|
term: &str,
|
||||||
let start = Instant::now();
|
sizes: IconSizes,
|
||||||
|
callback: F,
|
||||||
|
) {
|
||||||
|
if term.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let pattern = regex::escape(&term);
|
let pattern = regex::escape(&term);
|
||||||
let regex = match regex::RegexBuilder::new(&pattern)
|
let regex = match regex::RegexBuilder::new(&pattern)
|
||||||
|
|
@ -538,11 +546,10 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to parse regex {:?}: {}", pattern, err);
|
log::warn!("failed to parse regex {:?}: {}", pattern, err);
|
||||||
return Vec::new();
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let items_arc = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
//TODO: do we want to ignore files?
|
//TODO: do we want to ignore files?
|
||||||
ignore::WalkBuilder::new(tab_path)
|
ignore::WalkBuilder::new(tab_path)
|
||||||
//TODO: only use this on supported targets
|
//TODO: only use this on supported targets
|
||||||
|
|
@ -571,50 +578,19 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut items = items_arc.lock().unwrap();
|
if !callback(item_from_entry(
|
||||||
items.push(item_from_entry(
|
|
||||||
path.to_path_buf(),
|
path.to_path_buf(),
|
||||||
file_name.to_string(),
|
file_name.to_string(),
|
||||||
metadata,
|
metadata,
|
||||||
sizes,
|
sizes,
|
||||||
));
|
)) {
|
||||||
|
return ignore::WalkState::Quit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ignore::WalkState::Continue
|
ignore::WalkState::Continue
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut items = Arc::into_inner(items_arc).unwrap().into_inner().unwrap();
|
|
||||||
let duration = start.elapsed();
|
|
||||||
log::info!(
|
|
||||||
"searched for {:?} inside {:?} in {:?}, found {} items",
|
|
||||||
term,
|
|
||||||
tab_path,
|
|
||||||
duration,
|
|
||||||
items.len(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
|
|
||||||
items.par_sort_unstable_by(|a, b| {
|
|
||||||
let get_modified = |x: &Item| match &x.metadata {
|
|
||||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort with latest modified first
|
|
||||||
let a_modified = get_modified(a);
|
|
||||||
let b_modified = get_modified(b);
|
|
||||||
b_modified.cmp(&a_modified)
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = start.elapsed();
|
|
||||||
log::info!("sorted {} items in {:?}", items.len(), duration);
|
|
||||||
|
|
||||||
//TODO: ideal number of search results, pages?
|
|
||||||
items.truncate(100);
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This config statement is from trash::os_limited, inverted
|
// This config statement is from trash::os_limited, inverted
|
||||||
|
|
@ -786,8 +762,8 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
||||||
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
pub fn scan_network(uri: &str, sizes: IconSizes) -> Vec<Item> {
|
||||||
for (_key, mounter) in mounters.iter() {
|
for (_key, mounter) in MOUNTERS.iter() {
|
||||||
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)) => {
|
||||||
|
|
@ -802,9 +778,8 @@ pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item
|
||||||
//TODO: organize desktop items based on display
|
//TODO: organize desktop items based on display
|
||||||
pub fn scan_desktop(
|
pub fn scan_desktop(
|
||||||
tab_path: &PathBuf,
|
tab_path: &PathBuf,
|
||||||
display: &str,
|
_display: &str,
|
||||||
desktop_config: DesktopConfig,
|
desktop_config: DesktopConfig,
|
||||||
mounters: Mounters,
|
|
||||||
sizes: IconSizes,
|
sizes: IconSizes,
|
||||||
) -> Vec<Item> {
|
) -> Vec<Item> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
@ -814,7 +789,7 @@ 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_key, mounter) in MOUNTERS.iter() {
|
||||||
for mounter_item in mounter.items(sizes).unwrap_or_default() {
|
for mounter_item in mounter.items(sizes).unwrap_or_default() {
|
||||||
let Some(path) = mounter_item.path() else {
|
let Some(path) = mounter_item.path() else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -885,24 +860,26 @@ pub fn scan_desktop(
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum Location {
|
pub enum Location {
|
||||||
Desktop(PathBuf, String),
|
Desktop(PathBuf, String, DesktopConfig),
|
||||||
Network(String, String),
|
Network(String, String),
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
Recents,
|
Recents,
|
||||||
Search(PathBuf, String),
|
Search(PathBuf, String, Instant),
|
||||||
Trash,
|
Trash,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Location {
|
impl std::fmt::Display for Location {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(path, display) => write!(f, "{} on display {display}", path.display()),
|
Self::Desktop(path, display, ..) => {
|
||||||
Self::Network(uri, _) => write!(f, "{}", uri),
|
write!(f, "{} on display {display}", path.display())
|
||||||
|
}
|
||||||
|
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(path, term, ..) => write!(f, "search {} for {}", path.display(), term),
|
||||||
Self::Trash => write!(f, "trash"),
|
Self::Trash => write!(f, "trash"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -911,28 +888,37 @@ impl std::fmt::Display for Location {
|
||||||
impl Location {
|
impl Location {
|
||||||
pub fn path_opt(&self) -> Option<&PathBuf> {
|
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(path, _display) => Some(&path),
|
Self::Desktop(path, ..) => Some(&path),
|
||||||
Self::Path(path) => Some(&path),
|
Self::Path(path) => Some(&path),
|
||||||
Self::Search(path, _) => Some(&path),
|
Self::Search(path, ..) => Some(&path),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan(
|
pub fn with_path(&self, path: PathBuf) -> Self {
|
||||||
&self,
|
|
||||||
desktop_config: DesktopConfig,
|
|
||||||
mounters: Mounters,
|
|
||||||
sizes: IconSizes,
|
|
||||||
) -> Vec<Item> {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Desktop(path, display) => {
|
Self::Desktop(_, display, desktop_config) => {
|
||||||
scan_desktop(path, display, desktop_config, mounters, sizes)
|
Self::Desktop(path, display.clone(), *desktop_config)
|
||||||
|
}
|
||||||
|
Self::Path(..) => Self::Path(path),
|
||||||
|
Self::Search(_, term, ..) => Self::Search(path, term.clone(), Instant::now()),
|
||||||
|
other => other.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
|
||||||
|
match self {
|
||||||
|
Self::Desktop(path, display, desktop_config) => {
|
||||||
|
scan_desktop(path, display, *desktop_config, sizes)
|
||||||
}
|
}
|
||||||
Self::Path(path) => scan_path(path, sizes),
|
Self::Path(path) => scan_path(path, sizes),
|
||||||
Self::Search(path, term) => scan_search(path, term, sizes),
|
Self::Search(..) => {
|
||||||
|
// Search is done incrementally
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
Self::Trash => scan_trash(sizes),
|
Self::Trash => scan_trash(sizes),
|
||||||
Self::Recents => scan_recents(sizes),
|
Self::Recents => scan_recents(sizes),
|
||||||
Self::Network(uri, _) => scan_network(uri, mounters, sizes),
|
Self::Network(uri, _) => scan_network(uri, sizes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -990,6 +976,7 @@ pub enum Message {
|
||||||
MiddleClick(usize),
|
MiddleClick(usize),
|
||||||
Scroll(Viewport),
|
Scroll(Viewport),
|
||||||
ScrollToFocus,
|
ScrollToFocus,
|
||||||
|
SearchItem(Location, Item),
|
||||||
SelectAll,
|
SelectAll,
|
||||||
SetSort(HeadingOptions, bool),
|
SetSort(HeadingOptions, bool),
|
||||||
Thumbnail(PathBuf, ItemThumbnail),
|
Thumbnail(PathBuf, ItemThumbnail),
|
||||||
|
|
@ -1544,7 +1531,7 @@ impl Tab {
|
||||||
|
|
||||||
pub fn title(&self) -> String {
|
pub fn title(&self) -> String {
|
||||||
match &self.location {
|
match &self.location {
|
||||||
Location::Desktop(path, _display) => {
|
Location::Desktop(path, _, _) => {
|
||||||
let (name, _) = folder_name(path);
|
let (name, _) = folder_name(path);
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
@ -1552,7 +1539,7 @@ impl Tab {
|
||||||
let (name, _) = folder_name(path);
|
let (name, _) = folder_name(path);
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
Location::Search(path, term) => {
|
Location::Search(path, term, ..) => {
|
||||||
//TODO: translate
|
//TODO: translate
|
||||||
let (name, _) = folder_name(path);
|
let (name, _) = folder_name(path);
|
||||||
format!("Search \"{}\": {}", term, name)
|
format!("Search \"{}\": {}", term, name)
|
||||||
|
|
@ -1934,7 +1921,8 @@ impl Tab {
|
||||||
if let Some(range) = self.select_range {
|
if let Some(range) = self.select_range {
|
||||||
let min = range.0.min(range.1);
|
let min = range.0.min(range.1);
|
||||||
let max = range.0.max(range.1);
|
let max = range.0.max(range.1);
|
||||||
if self.sort_name == HeadingOptions::Name && self.sort_direction {
|
let (sort_name, sort_direction) = self.sort_options();
|
||||||
|
if sort_name == HeadingOptions::Name && sort_direction {
|
||||||
// A default/unsorted tab's view is consistent with how the
|
// A default/unsorted tab's view is consistent with how the
|
||||||
// Items are laid out internally (items_opt), so Items can be
|
// Items are laid out internally (items_opt), so Items can be
|
||||||
// linearly selected
|
// linearly selected
|
||||||
|
|
@ -2074,13 +2062,10 @@ impl Tab {
|
||||||
Message::LocationMenuAction(action) => {
|
Message::LocationMenuAction(action) => {
|
||||||
self.location_context_menu_index = None;
|
self.location_context_menu_index = None;
|
||||||
let path_for_index = |ancestor_index| {
|
let path_for_index = |ancestor_index| {
|
||||||
match self.location {
|
self.location
|
||||||
Location::Path(ref path) => Some(path),
|
.path_opt()
|
||||||
Location::Search(ref path, _) => Some(path),
|
.and_then(|path| path.ancestors().nth(ancestor_index))
|
||||||
_ => None,
|
.map(|path| path.to_path_buf())
|
||||||
}
|
|
||||||
.and_then(|path| path.ancestors().nth(ancestor_index))
|
|
||||||
.map(|path| path.to_path_buf())
|
|
||||||
};
|
};
|
||||||
match action {
|
match action {
|
||||||
LocationMenuAction::OpenInNewTab(ancestor_index) => {
|
LocationMenuAction::OpenInNewTab(ancestor_index) => {
|
||||||
|
|
@ -2461,6 +2446,27 @@ impl Tab {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::SearchItem(location, item) => {
|
||||||
|
if location == self.location {
|
||||||
|
if let Some(items) = &mut self.items_opt {
|
||||||
|
items.push(item);
|
||||||
|
} else {
|
||||||
|
log::warn!("tried to load items in {:?} without items array", location);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"search item found in {:?} instead of {:?}",
|
||||||
|
location,
|
||||||
|
self.location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: optimize
|
||||||
|
self.column_sort();
|
||||||
|
if let Some(items) = &mut self.items_opt {
|
||||||
|
items.truncate(MAX_SEARCH_RESULTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::SelectAll => {
|
Message::SelectAll => {
|
||||||
self.select_all();
|
self.select_all();
|
||||||
if self.select_focus.take().is_some() {
|
if self.select_focus.take().is_some() {
|
||||||
|
|
@ -2469,8 +2475,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SetSort(heading_option, dir) => {
|
Message::SetSort(heading_option, dir) => {
|
||||||
self.sort_name = heading_option;
|
if !matches!(self.location, Location::Search(..)) {
|
||||||
self.sort_direction = dir;
|
self.sort_name = heading_option;
|
||||||
|
self.sort_direction = dir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Thumbnail(path, thumbnail) => {
|
Message::Thumbnail(path, thumbnail) => {
|
||||||
if let Some(ref mut items) = self.items_opt {
|
if let Some(ref mut items) = self.items_opt {
|
||||||
|
|
@ -2509,14 +2517,16 @@ impl Tab {
|
||||||
self.config.view = view;
|
self.config.view = view;
|
||||||
}
|
}
|
||||||
Message::ToggleSort(heading_option) => {
|
Message::ToggleSort(heading_option) => {
|
||||||
let heading_sort = if self.sort_name == heading_option {
|
if !matches!(self.location, Location::Search(..)) {
|
||||||
!self.sort_direction
|
let heading_sort = if self.sort_name == heading_option {
|
||||||
} else {
|
!self.sort_direction
|
||||||
// Default modified to descending, and others to ascending.
|
} else {
|
||||||
heading_option != HeadingOptions::Modified
|
// Default modified to descending, and others to ascending.
|
||||||
};
|
heading_option != HeadingOptions::Modified
|
||||||
self.sort_direction = heading_sort;
|
};
|
||||||
self.sort_name = heading_option;
|
self.sort_direction = heading_sort;
|
||||||
|
self.sort_name = heading_option;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Drop(Some((to, mut from))) => {
|
Message::Drop(Some((to, mut from))) => {
|
||||||
self.dnd_hovered = None;
|
self.dnd_hovered = None;
|
||||||
|
|
@ -2608,11 +2618,7 @@ impl Tab {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if location != self.location {
|
} else if location != self.location {
|
||||||
if match &location {
|
if location.path_opt().map_or(true, |path| path.is_dir()) {
|
||||||
Location::Path(path) => path.is_dir(),
|
|
||||||
Location::Search(path, _term) => path.is_dir(),
|
|
||||||
_ => true,
|
|
||||||
} {
|
|
||||||
let prev_path = if let Some(path) = self.location.path_opt() {
|
let prev_path = if let Some(path) = self.location.path_opt() {
|
||||||
Some(path.to_path_buf())
|
Some(path.to_path_buf())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2629,6 +2635,13 @@ impl Tab {
|
||||||
commands
|
commands
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sort_options(&self) -> (HeadingOptions, bool) {
|
||||||
|
match self.location {
|
||||||
|
Location::Search(..) => (HeadingOptions::Modified, false),
|
||||||
|
_ => (self.sort_name, self.sort_direction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn column_sort(&self) -> Option<Vec<(usize, &Item)>> {
|
fn column_sort(&self) -> Option<Vec<(usize, &Item)>> {
|
||||||
let check_reverse = |ord: Ordering, sort: bool| {
|
let check_reverse = |ord: Ordering, sort: bool| {
|
||||||
if sort {
|
if sort {
|
||||||
|
|
@ -2638,8 +2651,8 @@ impl Tab {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut items: Vec<_> = self.items_opt.as_ref()?.iter().enumerate().collect();
|
let mut items: Vec<_> = self.items_opt.as_ref()?.iter().enumerate().collect();
|
||||||
let heading_sort = self.sort_direction;
|
let (sort_name, sort_direction) = self.sort_options();
|
||||||
match self.sort_name {
|
match sort_name {
|
||||||
HeadingOptions::Size => {
|
HeadingOptions::Size => {
|
||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
// entries take precedence over size
|
// entries take precedence over size
|
||||||
|
|
@ -2665,7 +2678,7 @@ impl Tab {
|
||||||
match (a_is_entry, b_is_entry) {
|
match (a_is_entry, b_is_entry) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
_ => check_reverse(a_size.cmp(&b_size), heading_sort),
|
_ => check_reverse(a_size.cmp(&b_size), sort_direction),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -2676,13 +2689,13 @@ impl Tab {
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
_ => check_reverse(
|
_ => check_reverse(
|
||||||
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
||||||
heading_sort,
|
sort_direction,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
check_reverse(
|
check_reverse(
|
||||||
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
LANGUAGE_SORTER.compare(&a.1.display_name, &b.1.display_name),
|
||||||
heading_sort,
|
sort_direction,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -2699,10 +2712,10 @@ impl Tab {
|
||||||
match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {
|
match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
_ => check_reverse(a_modified.cmp(&b_modified), heading_sort),
|
_ => check_reverse(a_modified.cmp(&b_modified), sort_direction),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
check_reverse(a_modified.cmp(&b_modified), heading_sort)
|
check_reverse(a_modified.cmp(&b_modified), sort_direction)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2719,10 +2732,10 @@ impl Tab {
|
||||||
match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {
|
match (a.1.metadata.is_dir(), b.1.metadata.is_dir()) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
_ => check_reverse(a_time_deleted.cmp(&b_time_deleted), heading_sort),
|
_ => check_reverse(a_time_deleted.cmp(&b_time_deleted), sort_direction),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
check_reverse(b_time_deleted.cmp(&a_time_deleted), heading_sort)
|
check_reverse(b_time_deleted.cmp(&a_time_deleted), sort_direction)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2977,13 +2990,14 @@ impl Tab {
|
||||||
let size_width = 100.0;
|
let size_width = 100.0;
|
||||||
let condensed = size.width < (name_width + modified_width + size_width);
|
let condensed = size.width < (name_width + modified_width + size_width);
|
||||||
|
|
||||||
|
let (sort_name, sort_direction) = self.sort_options();
|
||||||
let heading_item = |name, width, msg| {
|
let heading_item = |name, width, msg| {
|
||||||
let mut row = widget::row::with_capacity(2)
|
let mut row = widget::row::with_capacity(2)
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.width(width);
|
.width(width);
|
||||||
row = row.push(widget::text::heading(name));
|
row = row.push(widget::text::heading(name));
|
||||||
match (self.sort_name == msg, self.sort_direction) {
|
match (sort_name == msg, sort_direction) {
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
row = row.push(widget::icon::from_name("pan-down-symbolic").size(16));
|
row = row.push(widget::icon::from_name("pan-down-symbolic").size(16));
|
||||||
}
|
}
|
||||||
|
|
@ -3021,46 +3035,42 @@ impl Tab {
|
||||||
.spacing(space_xxs);
|
.spacing(space_xxs);
|
||||||
|
|
||||||
if let Some(location) = &self.edit_location {
|
if let Some(location) = &self.edit_location {
|
||||||
match location {
|
//TODO: allow editing other locations
|
||||||
Location::Path(path) => {
|
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),
|
||||||
)
|
)
|
||||||
.on_press(Message::EditLocation(None))
|
.on_press(Message::EditLocation(None))
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.style(theme::Button::Icon),
|
.style(theme::Button::Icon),
|
||||||
);
|
);
|
||||||
row = row.push(
|
row = row.push(
|
||||||
widget::text_input("", path.to_string_lossy())
|
widget::text_input("", path.to_string_lossy())
|
||||||
.id(self.edit_location_id.clone())
|
.id(self.edit_location_id.clone())
|
||||||
.on_input(|input| {
|
.on_input(|input| {
|
||||||
Message::EditLocation(Some(Location::Path(PathBuf::from(input))))
|
Message::EditLocation(Some(location.with_path(PathBuf::from(input))))
|
||||||
})
|
})
|
||||||
.on_submit(Message::Location(location.clone()))
|
.on_submit(Message::Location(location.clone()))
|
||||||
.line_height(1.0),
|
.line_height(1.0),
|
||||||
);
|
);
|
||||||
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(horizontal_rule(1).style(theme::Rule::Custom(Box::new(
|
column = column.push(horizontal_rule(1).style(theme::Rule::Custom(Box::new(
|
||||||
|theme: &Theme| rule::Appearance {
|
|theme: &Theme| rule::Appearance {
|
||||||
color: theme.cosmic().accent_color().into(),
|
color: theme.cosmic().accent_color().into(),
|
||||||
width: 1,
|
width: 1,
|
||||||
radius: 0.0.into(),
|
radius: 0.0.into(),
|
||||||
fill_mode: rule::FillMode::Full,
|
fill_mode: rule::FillMode::Full,
|
||||||
},
|
},
|
||||||
))));
|
))));
|
||||||
if self.config.view == View::List && !condensed {
|
if self.config.view == View::List && !condensed {
|
||||||
column = column.push(heading_row);
|
column = column.push(heading_row);
|
||||||
column = column.push(widget::divider::horizontal::default());
|
column = column.push(widget::divider::horizontal::default());
|
||||||
}
|
|
||||||
return column.into();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
//TODO: allow editing other locations
|
|
||||||
}
|
}
|
||||||
|
return column.into();
|
||||||
}
|
}
|
||||||
} else if let Location::Path(path) = &self.location {
|
} else if let Some(path) = self.location.path_opt() {
|
||||||
row = row.push(
|
row = row.push(
|
||||||
crate::mouse_area::MouseArea::new(
|
crate::mouse_area::MouseArea::new(
|
||||||
widget::button::custom(widget::icon::from_name("edit-symbolic").size(16))
|
widget::button::custom(widget::icon::from_name("edit-symbolic").size(16))
|
||||||
|
|
@ -3071,26 +3081,11 @@ impl Tab {
|
||||||
.on_middle_press(move |_| Message::OpenInNewTab(path.clone())),
|
.on_middle_press(move |_| Message::OpenInNewTab(path.clone())),
|
||||||
);
|
);
|
||||||
w += 16.0 + 2.0 * space_xxs as f32;
|
w += 16.0 + 2.0 * space_xxs as f32;
|
||||||
} else if let Location::Search(_, term) = &self.location {
|
|
||||||
row = row.push(
|
|
||||||
widget::button::custom(
|
|
||||||
widget::row::with_children(vec![
|
|
||||||
widget::icon::from_name("system-search-symbolic")
|
|
||||||
.size(16)
|
|
||||||
.into(),
|
|
||||||
widget::text::body(term).wrap(text::Wrap::None).into(),
|
|
||||||
])
|
|
||||||
.spacing(space_xxs),
|
|
||||||
)
|
|
||||||
.padding(space_xxs)
|
|
||||||
.style(theme::Button::Icon),
|
|
||||||
);
|
|
||||||
w += text_width_body(term) + 16.0 + 3.0 * space_xxs as f32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(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() {
|
||||||
|
|
@ -3131,14 +3126,7 @@ impl Tab {
|
||||||
w += name_width;
|
w += name_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = match &self.location {
|
let location = self.location.with_path(ancestor.to_path_buf());
|
||||||
Location::Path(_) => Location::Path(ancestor.to_path_buf()),
|
|
||||||
Location::Search(_, term) => {
|
|
||||||
Location::Search(ancestor.to_path_buf(), term.clone())
|
|
||||||
}
|
|
||||||
other => other.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut mouse_area = crate::mouse_area::MouseArea::new(
|
let mut mouse_area = crate::mouse_area::MouseArea::new(
|
||||||
widget::button::custom(row)
|
widget::button::custom(row)
|
||||||
.padding(space_xxxs)
|
.padding(space_xxxs)
|
||||||
|
|
@ -3251,7 +3239,7 @@ impl Tab {
|
||||||
.into(),
|
.into(),
|
||||||
widget::text(if has_hidden {
|
widget::text(if has_hidden {
|
||||||
fl!("empty-folder-hidden")
|
fl!("empty-folder-hidden")
|
||||||
} else if matches!(self.location, Location::Search(_, _)) {
|
} else if matches!(self.location, Location::Search(..)) {
|
||||||
fl!("no-results")
|
fl!("no-results")
|
||||||
} else {
|
} else {
|
||||||
fl!("empty-folder")
|
fl!("empty-folder")
|
||||||
|
|
@ -3583,7 +3571,7 @@ impl Tab {
|
||||||
let modified_width = 200.0;
|
let modified_width = 200.0;
|
||||||
let size_width = 100.0;
|
let size_width = 100.0;
|
||||||
let condensed = size.width < (name_width + modified_width + size_width);
|
let condensed = size.width < (name_width + modified_width + size_width);
|
||||||
let is_search = matches!(self.location, Location::Search(_, _));
|
let is_search = matches!(self.location, Location::Search(..));
|
||||||
let icon_size = if condensed || is_search {
|
let icon_size = if condensed || is_search {
|
||||||
icon_sizes.list_condensed()
|
icon_sizes.list_condensed()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -4056,86 +4044,123 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription(&self) -> Subscription<Message> {
|
pub fn subscription(&self) -> Subscription<Message> {
|
||||||
if let Some(items) = &self.items_opt {
|
let Some(items) = &self.items_opt else {
|
||||||
//TODO: how many thumbnail loads should be in flight at once?
|
return Subscription::none();
|
||||||
let jobs = 8;
|
};
|
||||||
let mut subscriptions = Vec::with_capacity(jobs);
|
|
||||||
|
|
||||||
//TODO: move to function
|
// Load search items incrementally
|
||||||
let visible_rect = {
|
if let Location::Search(path, term, start) = &self.location {
|
||||||
let point = match self.scroll_opt {
|
let location = self.location.clone();
|
||||||
Some(offset) => Point::new(0.0, offset.y),
|
let path = path.clone();
|
||||||
None => Point::new(0.0, 0.0),
|
let term = term.clone();
|
||||||
};
|
let start = start.clone();
|
||||||
let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));
|
return subscription::channel(location.clone(), 100, move |output| async move {
|
||||||
Rectangle::new(point, size)
|
let output = tokio::sync::Mutex::new(output);
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
//TODO: use correct icon sizes, or fetch icons lazily?
|
||||||
|
//TODO: getting mime types for search results is expensive, and not necessary if the results
|
||||||
|
// are not used. Perhaps they can be gathered when the item is scrolled to, like thumbnails
|
||||||
|
scan_search(&path, &term, IconSizes::default(), move |item| -> bool {
|
||||||
|
futures::executor::block_on(async {
|
||||||
|
output
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.send(Message::SearchItem(location.clone(), item))
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.is_ok()
|
||||||
|
});
|
||||||
|
log::info!(
|
||||||
|
"searched for {:?} in {:?} in {:?}",
|
||||||
|
term,
|
||||||
|
path,
|
||||||
|
start.elapsed(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::future::pending().await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: how many thumbnail loads should be in flight at once?
|
||||||
|
let jobs = 8;
|
||||||
|
let mut subscriptions = Vec::with_capacity(jobs);
|
||||||
|
|
||||||
|
//TODO: move to function
|
||||||
|
let visible_rect = {
|
||||||
|
let point = match self.scroll_opt {
|
||||||
|
Some(offset) => Point::new(0.0, offset.y),
|
||||||
|
None => Point::new(0.0, 0.0),
|
||||||
};
|
};
|
||||||
|
let size = self.size_opt.get().unwrap_or_else(|| Size::new(0.0, 0.0));
|
||||||
|
Rectangle::new(point, size)
|
||||||
|
};
|
||||||
|
|
||||||
//TODO: HACK to ensure positions are up to date since subscription runs before view
|
//TODO: HACK to ensure positions are up to date since subscription runs before view
|
||||||
match self.config.view {
|
match self.config.view {
|
||||||
View::Grid => _ = self.grid_view(),
|
View::Grid => _ = self.grid_view(),
|
||||||
View::List => _ = self.list_view(),
|
View::List => _ = self.list_view(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
if item.thumbnail_opt.is_some() {
|
if item.thumbnail_opt.is_some() {
|
||||||
// Skip items that already have a thumbnail
|
// Skip items that already have a thumbnail
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match item.rect_opt.get() {
|
match item.rect_opt.get() {
|
||||||
Some(rect) => {
|
Some(rect) => {
|
||||||
if !rect.intersects(&visible_rect) {
|
if !rect.intersects(&visible_rect) {
|
||||||
// Skip items that are not visible
|
// Skip items that are not visible
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Skip items with no determined rect (this should include hidden items)
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
if let Some(path) = item.path_opt().map(|path| path.to_path_buf()) {
|
// Skip items with no determined rect (this should include hidden items)
|
||||||
let mime = item.mime.clone();
|
continue;
|
||||||
subscriptions.push(subscription::channel(
|
|
||||||
path.clone(),
|
|
||||||
1,
|
|
||||||
|mut output| async move {
|
|
||||||
let (path, thumbnail) = tokio::task::spawn_blocking(move || {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
//TODO: configurable thumbnail size?
|
|
||||||
let thumbnail_size = (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32;
|
|
||||||
let thumbnail = ItemThumbnail::new(&path, mime, thumbnail_size);
|
|
||||||
log::debug!("thumbnailed {:?} in {:?}", path, start.elapsed());
|
|
||||||
(path, thumbnail)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
match output
|
|
||||||
.send(Message::Thumbnail(path.clone(), thumbnail))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to send thumbnail for {:?}: {}", path, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::future::pending().await
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if subscriptions.len() >= jobs {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Subscription::batch(subscriptions)
|
|
||||||
} else {
|
if let Some(path) = item.path_opt().map(|path| path.to_path_buf()) {
|
||||||
Subscription::none()
|
let mime = item.mime.clone();
|
||||||
|
subscriptions.push(subscription::channel(
|
||||||
|
path.clone(),
|
||||||
|
1,
|
||||||
|
|mut output| async move {
|
||||||
|
let (path, thumbnail) = tokio::task::spawn_blocking(move || {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
//TODO: configurable thumbnail size?
|
||||||
|
let thumbnail_size = (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32;
|
||||||
|
let thumbnail = ItemThumbnail::new(&path, mime, thumbnail_size);
|
||||||
|
log::debug!("thumbnailed {:?} in {:?}", path, start.elapsed());
|
||||||
|
(path, thumbnail)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match output
|
||||||
|
.send(Message::Thumbnail(path.clone(), thumbnail))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to send thumbnail for {:?}: {}", path, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::future::pending().await
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscriptions.len() >= jobs {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue