Compare commits
No commits in common. "deb6000c10d9974c964830a1d13c418af4681226" and "0f6f9509aee88091a51b67fe436aefc4f10692fe" have entirely different histories.
deb6000c10
...
0f6f9509ae
13 changed files with 218 additions and 773 deletions
411
Cargo.lock
generated
411
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files"
|
||||
version = "1.7.0"
|
||||
version = "1.6.0"
|
||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files-applet"
|
||||
version = "1.7.0"
|
||||
version = "1.6.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
6
debian/changelog
vendored
6
debian/changelog
vendored
|
|
@ -1,9 +1,3 @@
|
|||
cosmic-files (1.7.0) noble; urgency=medium
|
||||
|
||||
* Epoch 1.7.0 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Tue, 25 Aug 2026 09:20:22 -0600
|
||||
|
||||
cosmic-files (1.6.0) noble; urgency=medium
|
||||
|
||||
* Epoch 1.6.0 version update
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ no-results = Fandt ingen resultater
|
|||
filesystem = Filsystem
|
||||
home = Hjem
|
||||
networks = Netværk
|
||||
notification-in-progress = Fil-operationer er igang
|
||||
notification-in-progress = Fil-operationer er igang.
|
||||
trash = Papirkurv
|
||||
recents = Seneste
|
||||
undo = Fortryd
|
||||
|
|
@ -69,7 +69,7 @@ name-no-slashes = Navn må ikke indeholde skråstreger
|
|||
|
||||
## Open/Save Dialog
|
||||
|
||||
cancel = Afbryd
|
||||
cancel = Annuller
|
||||
create = Opret
|
||||
open = Åbn
|
||||
open-file = Åbn fil
|
||||
|
|
@ -95,7 +95,7 @@ rename-folder = Omdøb mappe
|
|||
## Replace Dialog
|
||||
|
||||
replace = Erstat
|
||||
replace-title = "{ $filename }" eksisterer allerede i denne placering
|
||||
replace-title = "{ $filename }" eksisterer allerede i denne placering.
|
||||
replace-warning = Ønsker du at erstatte den med den, du er ved at gemme? Erstatning af den vil overskrive dens indhold.
|
||||
replace-warning-operation = Ønsker du at erstatte den? Erstatning af den vil overskrive dens indhold.
|
||||
original-file = Original fil
|
||||
|
|
@ -253,7 +253,7 @@ light = Lyst
|
|||
# Context menu
|
||||
add-to-sidebar = Tilføj til sidebjælke
|
||||
compress = Komprimer…
|
||||
extract-here = Uddrag
|
||||
extract-here = Extract
|
||||
new-file = Ny fil...
|
||||
new-folder = Ny mappe...
|
||||
open-in-terminal = Åbn i terminal
|
||||
|
|
@ -332,7 +332,7 @@ deleted =
|
|||
Slettede { $items } { $items ->
|
||||
[one] element
|
||||
*[other] elementer
|
||||
} fra { trash }
|
||||
} fra papirkurven
|
||||
removing-from-recents =
|
||||
Fjerner { $items } { $items ->
|
||||
[one] element
|
||||
|
|
@ -360,7 +360,7 @@ remove = Fjern
|
|||
context-action = Konteksthandling
|
||||
context-action-confirm-title = Kør “{ $name }”?
|
||||
context-action-confirm-warning =
|
||||
Dette vil køre på { $items } { $items ->
|
||||
This will run on { $items } { $items ->
|
||||
[one] item
|
||||
*[other] items
|
||||
}.
|
||||
|
|
|
|||
|
|
@ -119,9 +119,6 @@ rename-file = Rename file
|
|||
rename-folder = Rename folder
|
||||
rename-confirm = Rename
|
||||
|
||||
## Change Sidebar Label Dialog
|
||||
sidebar-label = Sidebar label
|
||||
|
||||
## Replace Dialog
|
||||
replace = Replace
|
||||
replace-title = "{$filename}" already exists in this location
|
||||
|
|
@ -302,7 +299,6 @@ unknown-folder = unknown folder
|
|||
## Open with
|
||||
menu-open-with = Open with...
|
||||
default-app = {$name} (default)
|
||||
search-application = Search by app name
|
||||
|
||||
## Show details
|
||||
show-details = Show details
|
||||
|
|
@ -351,7 +347,6 @@ move-to = Move to...
|
|||
move-to-trash = Move to trash
|
||||
restore-from-trash = Restore from trash
|
||||
remove-from-sidebar = Remove from sidebar
|
||||
change-sidebar-label = Change sidebar label
|
||||
sort-by-name = Sort by name
|
||||
sort-by-modified = Sort by modified
|
||||
sort-by-size = Sort by size
|
||||
|
|
|
|||
|
|
@ -109,10 +109,6 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target
|
|||
rename-file = Renomear arquivo
|
||||
rename-folder = Renomear pasta
|
||||
|
||||
## Change Sidebar Label Dialog
|
||||
|
||||
sidebar-label = Rótulo da barra lateral
|
||||
|
||||
## Replace Dialog
|
||||
|
||||
replace = Substituir
|
||||
|
|
@ -354,7 +350,7 @@ light = Estilo claro
|
|||
|
||||
### Type to Search
|
||||
|
||||
type-to-search = Digitar para pesquisar
|
||||
type-to-search = Digite para pesquisar
|
||||
type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas
|
||||
type-to-search-enter-path = Insere o caminho do diretório ou arquivo
|
||||
# Context menu
|
||||
|
|
@ -369,7 +365,6 @@ open-in-terminal = Abrir no terminal
|
|||
move-to-trash = Mover para a lixeira
|
||||
restore-from-trash = Restaurar da lixeira
|
||||
remove-from-sidebar = Remover da barra lateral
|
||||
change-sidebar-label = Alterar rótulo na barra lateral
|
||||
sort-by-name = Ordenar por nome
|
||||
sort-by-modified = Ordenar por data de modificação
|
||||
sort-by-size = Ordenar por tamanho
|
||||
|
|
|
|||
|
|
@ -174,7 +174,6 @@ unknown-folder = неизвестная папка
|
|||
|
||||
menu-open-with = Открыть с помощью…
|
||||
default-app = { $name } (по умолчанию)
|
||||
search-application = Поиск по названию приложения
|
||||
|
||||
## Show details
|
||||
|
||||
|
|
|
|||
210
src/app.rs
210
src/app.rs
|
|
@ -37,7 +37,7 @@ use cosmic::widget::menu::action::MenuAction;
|
|||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::segmented_button::{self, Entity, ReorderEvent};
|
||||
use cosmic::widget::{self, icon, settings, space};
|
||||
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, surface, theme};
|
||||
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme};
|
||||
use mime_guess::Mime;
|
||||
use notify_debouncer_full::notify::{self, RecommendedWatcher};
|
||||
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
|
||||
|
|
@ -428,7 +428,6 @@ pub enum NavMenuAction {
|
|||
Preview(segmented_button::Entity),
|
||||
RunContextAction(segmented_button::Entity, usize),
|
||||
RemoveFromSidebar(segmented_button::Entity),
|
||||
ChangeSidebarLabel(segmented_button::Entity),
|
||||
}
|
||||
|
||||
impl MenuAction for NavMenuAction {
|
||||
|
|
@ -504,7 +503,6 @@ pub enum Message {
|
|||
OpenWithBrowse,
|
||||
OpenWithDialog(Option<Entity>),
|
||||
OpenWithSelection(usize),
|
||||
OpenWithSearchClear,
|
||||
OpenWithToggleDefault(bool),
|
||||
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
|
||||
Overlap(window::Id, OverlapNotifyEvent),
|
||||
|
|
@ -693,7 +691,6 @@ pub enum DialogPage {
|
|||
mime: mime_guess::Mime,
|
||||
selected: usize,
|
||||
store_opt: Option<Arc<MimeApp>>,
|
||||
search_app_name: String,
|
||||
/// When true, the chosen app is written to mimeapps.list as the
|
||||
/// default handler for `mime` after it spawns.
|
||||
set_default: bool,
|
||||
|
|
@ -704,10 +701,6 @@ pub enum DialogPage {
|
|||
DeleteTrash {
|
||||
items: Vec<TrashItem>,
|
||||
},
|
||||
ChangeSidebarLabel {
|
||||
entity: Entity,
|
||||
label: String,
|
||||
},
|
||||
RenameItem {
|
||||
from: PathBuf,
|
||||
parent: PathBuf,
|
||||
|
|
@ -1963,9 +1956,15 @@ impl App {
|
|||
|
||||
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
|
||||
if let Some(path) = favorite.path_opt() {
|
||||
let name = favorite
|
||||
.display_name()
|
||||
.unwrap_or_else(|| fl!("filesystem"));
|
||||
let name = if matches!(favorite, Favorite::Home) {
|
||||
fl!("home")
|
||||
} else if let Favorite::Network { name, .. } = favorite {
|
||||
name.clone()
|
||||
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
||||
file_name.to_string()
|
||||
} else {
|
||||
fl!("filesystem")
|
||||
};
|
||||
nav_model = nav_model.insert(move |b| {
|
||||
b.text(name.clone())
|
||||
.icon(
|
||||
|
|
@ -2602,33 +2601,15 @@ impl App {
|
|||
.favorites
|
||||
.iter()
|
||||
.map(|favorite| {
|
||||
match favorite {
|
||||
Favorite::Path(path) => {
|
||||
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
|
||||
if let Favorite::Path(path) = favorite {
|
||||
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) {
|
||||
if path.starts_with(from)
|
||||
&& let Ok(relative) = path.strip_prefix(from)
|
||||
{
|
||||
if path.starts_with(from)
|
||||
&& let Ok(relative) = path.strip_prefix(from)
|
||||
{
|
||||
favorites_changed = true;
|
||||
return Favorite::from_path(to.join(relative));
|
||||
}
|
||||
favorites_changed = true;
|
||||
return Favorite::from_path(to.join(relative));
|
||||
}
|
||||
}
|
||||
Favorite::Named { path, name } => {
|
||||
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
|
||||
{
|
||||
if path.starts_with(from)
|
||||
&& let Ok(relative) = path.strip_prefix(from)
|
||||
{
|
||||
favorites_changed = true;
|
||||
return Favorite::Named {
|
||||
path: to.join(relative),
|
||||
name: name.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
favorite.clone()
|
||||
})
|
||||
|
|
@ -2944,11 +2925,6 @@ impl Application for App {
|
|||
}
|
||||
items.push(cosmic::widget::menu::Item::Divider);
|
||||
if favorite_index_opt.is_some() {
|
||||
items.push(cosmic::widget::menu::Item::Button(
|
||||
fl!("change-sidebar-label"),
|
||||
None,
|
||||
NavMenuAction::ChangeSidebarLabel(entity),
|
||||
));
|
||||
items.push(cosmic::widget::menu::Item::Button(
|
||||
fl!("remove-from-sidebar"),
|
||||
None,
|
||||
|
|
@ -3240,8 +3216,7 @@ impl Application for App {
|
|||
} else {
|
||||
Favorite::from_path(path)
|
||||
};
|
||||
let favorite_path = favorite.path_opt();
|
||||
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
|
||||
if !favorites.contains(&favorite) {
|
||||
favorites.push(favorite);
|
||||
}
|
||||
}
|
||||
|
|
@ -3548,18 +3523,10 @@ impl Application for App {
|
|||
path,
|
||||
mime,
|
||||
selected,
|
||||
search_app_name,
|
||||
set_default,
|
||||
..
|
||||
} => {
|
||||
let mut available_apps =
|
||||
self.mime_app_cache.get_apps_for_mime(&mime, true);
|
||||
available_apps.retain(|(app, _)| {
|
||||
app.name
|
||||
.to_lowercase()
|
||||
.trim()
|
||||
.contains(search_app_name.to_lowercase().as_str().trim())
|
||||
});
|
||||
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true);
|
||||
|
||||
if let Some((app, _)) = available_apps.get(selected) {
|
||||
if let Some(mut command) =
|
||||
|
|
@ -3605,18 +3572,6 @@ impl Application for App {
|
|||
DialogPage::DeleteTrash { items } => {
|
||||
tasks.push(self.operation(Operation::DeleteTrash { items }));
|
||||
}
|
||||
DialogPage::ChangeSidebarLabel { entity, label } => {
|
||||
if let Some(FavoriteIndex(favorite_i)) =
|
||||
self.nav_model.data::<FavoriteIndex>(entity)
|
||||
{
|
||||
let mut favorites = self.config.favorites.clone();
|
||||
if let Some(favorite) = favorites.get_mut(*favorite_i) {
|
||||
*favorite = favorite.with_label(label.trim());
|
||||
config_set!(favorites, favorites);
|
||||
tasks.push(self.update_config());
|
||||
}
|
||||
}
|
||||
}
|
||||
DialogPage::RenameItem {
|
||||
from, parent, name, ..
|
||||
} => {
|
||||
|
|
@ -4218,25 +4173,21 @@ impl Application for App {
|
|||
let Some(path) = item.path_opt() else {
|
||||
continue;
|
||||
};
|
||||
return Task::batch([
|
||||
self.push_dialog(
|
||||
DialogPage::OpenWith {
|
||||
path: path.clone(),
|
||||
mime: item.mime.clone(),
|
||||
selected: 0,
|
||||
store_opt: "x-scheme-handler/mime"
|
||||
.parse::<mime_guess::Mime>()
|
||||
.ok()
|
||||
.and_then(|mime| {
|
||||
self.mime_app_cache.get(&mime).first().cloned()
|
||||
}),
|
||||
search_app_name: String::new(),
|
||||
set_default: false,
|
||||
},
|
||||
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
||||
),
|
||||
widget::text_input::focus(self.dialog_text_input.clone()),
|
||||
]);
|
||||
return self.push_dialog(
|
||||
DialogPage::OpenWith {
|
||||
path: path.clone(),
|
||||
mime: item.mime.clone(),
|
||||
selected: 0,
|
||||
store_opt: "x-scheme-handler/mime"
|
||||
.parse::<mime_guess::Mime>()
|
||||
.ok()
|
||||
.and_then(|mime| {
|
||||
self.mime_app_cache.get(&mime).first().cloned()
|
||||
}),
|
||||
set_default: false,
|
||||
},
|
||||
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4252,14 +4203,6 @@ impl Application for App {
|
|||
*set_default = enabled;
|
||||
}
|
||||
}
|
||||
Message::OpenWithSearchClear => {
|
||||
if let Some(DialogPage::OpenWith {
|
||||
search_app_name, ..
|
||||
}) = self.dialog_pages.front_mut()
|
||||
{
|
||||
*search_app_name = String::new();
|
||||
}
|
||||
}
|
||||
Message::Paste(entity_opt) => {
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
|
||||
|
|
@ -4945,8 +4888,7 @@ impl Application for App {
|
|||
tab::Command::AddToSidebar(path) => {
|
||||
let mut favorites = self.config.favorites.clone();
|
||||
let favorite = Favorite::from_path(path);
|
||||
let favorite_path = favorite.path_opt();
|
||||
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
|
||||
if !favorites.contains(&favorite) {
|
||||
favorites.push(favorite);
|
||||
}
|
||||
config_set!(favorites, favorites);
|
||||
|
|
@ -5595,7 +5537,6 @@ impl Application for App {
|
|||
.and_then(|mime| {
|
||||
self.mime_app_cache.get(&mime).first().cloned()
|
||||
}),
|
||||
search_app_name: String::new(),
|
||||
set_default: false,
|
||||
},
|
||||
None,
|
||||
|
|
@ -5731,26 +5672,6 @@ impl Application for App {
|
|||
return self.update_config();
|
||||
}
|
||||
}
|
||||
|
||||
NavMenuAction::ChangeSidebarLabel(entity) => {
|
||||
if let Some(favorite) = self
|
||||
.nav_model
|
||||
.data::<FavoriteIndex>(entity)
|
||||
.and_then(|FavoriteIndex(favorite_i)| {
|
||||
self.config.favorites.get(*favorite_i)
|
||||
})
|
||||
{
|
||||
let label = favorite
|
||||
.display_name()
|
||||
.unwrap_or_else(|| fl!("filesystem"));
|
||||
return Task::batch([
|
||||
self.dialog_pages
|
||||
.push_back(DialogPage::ChangeSidebarLabel { entity, label }),
|
||||
widget::text_input::focus(self.dialog_text_input.clone()),
|
||||
widget::text_input::select_all(self.dialog_text_input.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
Message::Recents => {
|
||||
if self.config.show_recents {
|
||||
|
|
@ -6473,9 +6394,7 @@ impl Application for App {
|
|||
mime,
|
||||
selected,
|
||||
store_opt,
|
||||
search_app_name,
|
||||
set_default,
|
||||
..
|
||||
} => {
|
||||
let name = match path.file_name() {
|
||||
Some(file_name) => file_name.to_str(),
|
||||
|
|
@ -6483,13 +6402,7 @@ impl Application for App {
|
|||
};
|
||||
|
||||
let mut column = widget::list_column();
|
||||
let mut available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
|
||||
available_apps.retain(|(app, _)| {
|
||||
app.name
|
||||
.to_lowercase()
|
||||
.trim()
|
||||
.contains(search_app_name.to_lowercase().as_str().trim())
|
||||
});
|
||||
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
|
||||
let item_height = 32.0;
|
||||
let mut displayed_default = false;
|
||||
let mut last_kind = MimeAppMatch::Exact;
|
||||
|
|
@ -6553,25 +6466,6 @@ impl Application for App {
|
|||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
)
|
||||
.control(
|
||||
widget::text_input::search_input(
|
||||
fl!("search-application"),
|
||||
search_app_name,
|
||||
)
|
||||
.id(self.dialog_text_input.clone())
|
||||
.on_clear(Message::OpenWithSearchClear)
|
||||
.on_input(move |search_app_name| {
|
||||
Message::DialogUpdate(DialogPage::OpenWith {
|
||||
path: path.clone(),
|
||||
mime: mime.clone(),
|
||||
selected: *selected,
|
||||
store_opt: store_opt.clone(),
|
||||
search_app_name,
|
||||
set_default: *set_default,
|
||||
})
|
||||
})
|
||||
.on_submit(|_| Message::DialogComplete),
|
||||
)
|
||||
.control(widget::scrollable(column).height({
|
||||
let max_size = self
|
||||
.size
|
||||
|
|
@ -6660,40 +6554,6 @@ impl Application for App {
|
|||
target = target
|
||||
)))
|
||||
}
|
||||
DialogPage::ChangeSidebarLabel { entity, label } => {
|
||||
let entity = *entity;
|
||||
let complete_maybe = if label.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Message::DialogComplete)
|
||||
};
|
||||
|
||||
widget::dialog()
|
||||
.title(fl!("change-sidebar-label"))
|
||||
.primary_action(
|
||||
widget::button::suggested(fl!("save"))
|
||||
.on_press_maybe(complete_maybe.clone()),
|
||||
)
|
||||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||
)
|
||||
.control(
|
||||
widget::column::with_children([
|
||||
widget::text::body(fl!("sidebar-label")).into(),
|
||||
widget::text_input("", label.as_str())
|
||||
.id(self.dialog_text_input.clone())
|
||||
.on_input(move |label| {
|
||||
Message::DialogUpdate(DialogPage::ChangeSidebarLabel {
|
||||
entity,
|
||||
label,
|
||||
})
|
||||
})
|
||||
.on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
|
||||
.into(),
|
||||
])
|
||||
.spacing(space_xxs),
|
||||
)
|
||||
}
|
||||
DialogPage::RenameItem {
|
||||
from,
|
||||
parent,
|
||||
|
|
|
|||
100
src/config.rs
100
src/config.rs
|
|
@ -70,11 +70,6 @@ pub enum Favorite {
|
|||
name: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
/// A path with a custom name chosen by the user
|
||||
Named {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Favorite {
|
||||
|
|
@ -103,38 +98,6 @@ impl Favorite {
|
|||
Self::Videos => dirs::video_dir(),
|
||||
Self::Path(path) => Some(path.clone()),
|
||||
Self::Network { path, .. } => Some(path.clone()),
|
||||
Self::Named { path, .. } => Some(path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Name shown in the sidebar, or `None` if the path has no usable file name
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Home => Some(crate::fl!("home")),
|
||||
Self::Named { name, .. } | Self::Network { name, .. } => Some(name.clone()),
|
||||
_ => self
|
||||
.path_opt()?
|
||||
.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return this favorite with a custom sidebar label chosen by the user
|
||||
pub fn with_label(&self, name: &str) -> Self {
|
||||
match self {
|
||||
Self::Network { uri, path, .. } => Self::Network {
|
||||
uri: uri.clone(),
|
||||
name: name.to_string(),
|
||||
path: path.clone(),
|
||||
},
|
||||
other => match other.path_opt() {
|
||||
Some(path) => Self::Named {
|
||||
path,
|
||||
name: name.to_string(),
|
||||
},
|
||||
None => other.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -513,66 +476,3 @@ pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime";
|
|||
pub struct TimeConfig {
|
||||
pub military_time: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn favorite_with_label_converts_path_to_named() {
|
||||
let favorite = Favorite::Path(PathBuf::from("/some/dir"));
|
||||
assert_eq!(
|
||||
favorite.with_label("Custom"),
|
||||
Favorite::Named {
|
||||
path: PathBuf::from("/some/dir"),
|
||||
name: "Custom".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorite_with_label_updates_network_in_place() {
|
||||
let favorite = Favorite::Network {
|
||||
uri: "sftp://example.com/".to_string(),
|
||||
name: "example.com".to_string(),
|
||||
path: PathBuf::from("/run/mount/example"),
|
||||
};
|
||||
assert_eq!(
|
||||
favorite.with_label("Custom"),
|
||||
Favorite::Network {
|
||||
uri: "sftp://example.com/".to_string(),
|
||||
name: "Custom".to_string(),
|
||||
path: PathBuf::from("/run/mount/example"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorite_with_label_converts_special_folder_to_named() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
assert_eq!(
|
||||
Favorite::Home.with_label("Custom"),
|
||||
Favorite::Named {
|
||||
path: home,
|
||||
name: "Custom".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorite_display_name() {
|
||||
assert_eq!(
|
||||
Favorite::Path(PathBuf::from("/some/dir")).display_name(),
|
||||
Some("dir".to_string())
|
||||
);
|
||||
assert_eq!(Favorite::Path(PathBuf::from("/")).display_name(), None);
|
||||
assert_eq!(
|
||||
Favorite::Named {
|
||||
path: PathBuf::from("/some/dir"),
|
||||
name: "Custom".to_string(),
|
||||
}
|
||||
.display_name(),
|
||||
Some("Custom".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ use std::{env, fmt, fs};
|
|||
use crate::app::{
|
||||
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
|
||||
};
|
||||
use crate::config::{Config, DialogConfig, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch};
|
||||
use crate::config::{
|
||||
Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
|
||||
};
|
||||
use crate::key_bind::key_binds;
|
||||
use crate::localize::LANGUAGE_SORTER;
|
||||
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
||||
|
|
@ -893,7 +895,13 @@ impl App {
|
|||
|
||||
for favorite in &self.flags.config.favorites {
|
||||
if let Some(path) = favorite.path_opt() {
|
||||
let Some(name) = favorite.display_name() else {
|
||||
let name = if matches!(favorite, Favorite::Home) {
|
||||
fl!("home")
|
||||
} else if let Favorite::Network { name, .. } = favorite {
|
||||
name.clone()
|
||||
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
||||
file_name.to_string()
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
nav_model = nav_model.insert(move |b| {
|
||||
|
|
|
|||
|
|
@ -18,15 +18,6 @@ use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Loca
|
|||
|
||||
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
|
||||
|
||||
// Attributes requested when listing a network directory.
|
||||
const SCAN_ATTRIBUTES: &str = "standard::name,\
|
||||
standard::display-name,\
|
||||
standard::type,\
|
||||
standard::size,\
|
||||
standard::icon,\
|
||||
standard::is-hidden,\
|
||||
time::modified";
|
||||
|
||||
fn resolve_uri(uri: &str) -> (String, gio::File) {
|
||||
let file = gio::File::for_uri(uri);
|
||||
// Resolve the target-uri if it exists
|
||||
|
|
@ -128,27 +119,9 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
|||
Box::from([])
|
||||
};
|
||||
|
||||
// `filesystem::remote` belongs to the filesystem namespace, which `enumerate_children`
|
||||
// never fills in, so it has to be queried once for the directory being listed.
|
||||
let remote = file
|
||||
.query_filesystem_info(
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
||||
gio::Cancellable::NONE,
|
||||
)
|
||||
.map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
||||
.unwrap_or_else(|err| {
|
||||
log::warn!("failed to get GIO filesystem info for {uri}: {err}");
|
||||
// Assume remote, so per-entry work is skipped rather than retried
|
||||
true
|
||||
});
|
||||
|
||||
let mut items = Vec::new();
|
||||
for info_res in file
|
||||
.enumerate_children(
|
||||
SCAN_ATTRIBUTES,
|
||||
gio::FileQueryInfoFlags::NONE,
|
||||
gio::Cancellable::NONE,
|
||||
)
|
||||
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
|
||||
.map_err(err_str)?
|
||||
{
|
||||
let info = info_res.map_err(err_str)?;
|
||||
|
|
@ -160,17 +133,13 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
|||
//TODO: what is the best way to resolve shortcuts?
|
||||
let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
|
||||
|
||||
let metadata = if force_dir {
|
||||
ItemMetadata::SimpleDir { entries: 0 }
|
||||
} else {
|
||||
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) {
|
||||
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
||||
let is_dir = matches!(info.file_type(), gio::FileType::Directory);
|
||||
let size_opt = (!is_dir).then_some(info.size() as u64);
|
||||
let mut children_opt = None;
|
||||
|
||||
// Counting children costs a directory listing per entry, which is far too
|
||||
// expensive on a remote filesystem
|
||||
if is_dir && !remote {
|
||||
if is_dir {
|
||||
if let Some(path) = file.child(&name).path() {
|
||||
//TODO: calculate children in the background (and make it cancellable?)
|
||||
match std::fs::read_dir(&path) {
|
||||
|
|
@ -190,8 +159,9 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
|||
mtime,
|
||||
size_opt,
|
||||
children_opt,
|
||||
is_dir,
|
||||
}
|
||||
} else {
|
||||
ItemMetadata::SimpleDir { entries: 0 }
|
||||
};
|
||||
|
||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||
|
|
|
|||
183
src/tab.rs
183
src/tab.rs
|
|
@ -458,9 +458,7 @@ impl<'a> FormatTime<'a> {
|
|||
|
||||
impl Display for FormatTime<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Ok(zoned) = jiff::Zoned::try_from(self.time) else {
|
||||
return Ok(());
|
||||
};
|
||||
let zoned = jiff::Zoned::try_from(self.time).unwrap();
|
||||
let now = jiff::Zoned::now();
|
||||
let icu_datetime = DateTime::convert_from(zoned.datetime());
|
||||
if zoned.date() == now.date() {
|
||||
|
|
@ -663,41 +661,6 @@ fn display_name_for_file(path: &Path, name: &str, get_from_gvfs: bool, is_deskto
|
|||
Item::display_name(name)
|
||||
}
|
||||
|
||||
// Whether a dir lives on a remote filesystem, according to GIO.
|
||||
#[cfg(feature = "gvfs")]
|
||||
fn gvfs_dir_is_remote(dir: &Path) -> bool {
|
||||
static REMOTE_CACHE: LazyLock<RwLock<FxHashMap<PathBuf, bool>>> =
|
||||
LazyLock::new(|| RwLock::new(FxHashMap::default()));
|
||||
|
||||
if let Some(remote) = REMOTE_CACHE.read().unwrap().get(dir) {
|
||||
return *remote;
|
||||
}
|
||||
|
||||
let remote = match gio::prelude::FileExt::query_filesystem_info(
|
||||
&gio::File::for_path(dir),
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to get GIO filesystem info for {}: {}",
|
||||
dir.display(),
|
||||
err
|
||||
);
|
||||
|
||||
//Assume remote so that the "expensive tasks" are rather skipped then actually executed.
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
REMOTE_CACHE
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(dir.to_path_buf(), remote);
|
||||
remote
|
||||
}
|
||||
|
||||
#[cfg(feature = "gvfs")]
|
||||
pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item {
|
||||
let file_name = file_info
|
||||
|
|
@ -705,7 +668,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
|||
.unwrap_or_default();
|
||||
let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
|
||||
let mut is_desktop = false;
|
||||
let remote = path.parent().is_none_or(gvfs_dir_is_remote);
|
||||
let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE);
|
||||
let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);
|
||||
|
||||
let size_opt = (!is_dir).then_some(file_info.size() as u64);
|
||||
|
|
@ -766,7 +729,6 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
|||
mtime,
|
||||
size_opt,
|
||||
children_opt,
|
||||
is_dir,
|
||||
},
|
||||
hidden,
|
||||
image_dimensions: (!remote && mime.type_() == mime::IMAGE)
|
||||
|
|
@ -821,7 +783,23 @@ pub fn item_from_entry(
|
|||
#[cfg(feature = "gvfs")]
|
||||
FsKind::Gvfs => {
|
||||
is_gvfs = true;
|
||||
path.parent().is_none_or(gvfs_dir_is_remote)
|
||||
let file = gio::File::for_path(&path);
|
||||
|
||||
match gio::prelude::FileExt::query_filesystem_info(
|
||||
&file,
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to get GIO filesystem info for {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "gvfs"))]
|
||||
FsKind::Gvfs => {
|
||||
|
|
@ -1046,6 +1024,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
|||
// gio crate expects a comma delimited string
|
||||
let attr_string = [
|
||||
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
|
||||
|
|
@ -1854,8 +1833,6 @@ pub enum Message {
|
|||
HighlightDeactivate(usize),
|
||||
HighlightActivate(usize),
|
||||
DirectorySize(PathBuf, DirSize),
|
||||
#[cfg(feature = "gvfs")]
|
||||
DirectoryChildren(PathBuf, usize),
|
||||
Checksums(PathBuf, ChecksumState),
|
||||
CalculateChecksums(PathBuf),
|
||||
CopyChecksum(String),
|
||||
|
|
@ -1924,7 +1901,6 @@ pub enum ItemMetadata {
|
|||
mtime: u64,
|
||||
size_opt: Option<u64>,
|
||||
children_opt: Option<usize>,
|
||||
is_dir: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1939,7 +1915,7 @@ impl ItemMetadata {
|
|||
Self::SimpleDir { .. } => true,
|
||||
Self::SimpleFile { .. } => false,
|
||||
#[cfg(feature = "gvfs")]
|
||||
Self::GvfsPath { is_dir, .. } => *is_dir,
|
||||
Self::GvfsPath { children_opt, .. } => children_opt.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1948,7 +1924,7 @@ impl ItemMetadata {
|
|||
Self::Path { metadata, .. } => metadata.modified().ok(),
|
||||
#[cfg(feature = "gvfs")]
|
||||
Self::GvfsPath { mtime, .. } => {
|
||||
SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(*mtime))
|
||||
Some(SystemTime::UNIX_EPOCH + Duration::from_secs(*mtime))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -2370,7 +2346,6 @@ impl Item {
|
|||
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||
widget::text::body(name)
|
||||
.wrapping(text::Wrapping::WordOrGlyph)
|
||||
.align_x(text::Alignment::Center)
|
||||
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
|
||||
3,
|
||||
)))
|
||||
|
|
@ -4867,20 +4842,6 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "gvfs")]
|
||||
Message::DirectoryChildren(path, children) => {
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
if item.path_opt() == Some(&path) {
|
||||
if let ItemMetadata::GvfsPath { children_opt, .. } = &mut item.metadata
|
||||
{
|
||||
*children_opt = Some(children);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Checksums(path, checksum_state) => {
|
||||
let location = Location::Path(path);
|
||||
if let Some(ref mut item) = self.parent_item_opt
|
||||
|
|
@ -5042,15 +5003,11 @@ impl Tab {
|
|||
ItemMetadata::GvfsPath {
|
||||
size_opt,
|
||||
children_opt,
|
||||
is_dir,
|
||||
..
|
||||
} => {
|
||||
if *is_dir {
|
||||
(true, children_opt.unwrap_or_default() as u64)
|
||||
} else {
|
||||
(false, size_opt.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
} => match children_opt {
|
||||
Some(child_count) => (true, *child_count as u64),
|
||||
None => (false, size_opt.unwrap_or_default()),
|
||||
},
|
||||
};
|
||||
let (a_is_entry, a_size) = get_size(a.1);
|
||||
let (b_is_entry, b_size) = get_size(b.1);
|
||||
|
|
@ -6228,21 +6185,17 @@ impl Tab {
|
|||
ItemMetadata::GvfsPath {
|
||||
size_opt,
|
||||
children_opt,
|
||||
is_dir,
|
||||
..
|
||||
} => {
|
||||
if *is_dir {
|
||||
// Children are not counted on remote filesystems
|
||||
match children_opt {
|
||||
//TODO: translate
|
||||
Some(1) => "1 item".to_string(),
|
||||
Some(child_count) => format!("{child_count} items"),
|
||||
None => String::new(),
|
||||
} => match children_opt {
|
||||
Some(child_count) => {
|
||||
if *child_count == 1 {
|
||||
format!("{child_count} item")
|
||||
} else {
|
||||
format!("{child_count} items")
|
||||
}
|
||||
} else {
|
||||
format_size(size_opt.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
None => format_size(size_opt.unwrap_or_default()),
|
||||
},
|
||||
};
|
||||
|
||||
let row = if condensed {
|
||||
|
|
@ -7008,74 +6961,6 @@ impl Tab {
|
|||
Rectangle::new(point, size)
|
||||
};
|
||||
|
||||
// Count the children of visible directories in the background. Doing it while
|
||||
// scanning costs one directory listing per entry, which stalls remote filesystems.
|
||||
#[cfg(feature = "gvfs")]
|
||||
for item in items {
|
||||
let ItemMetadata::GvfsPath {
|
||||
children_opt: None,
|
||||
is_dir: true,
|
||||
..
|
||||
} = &item.metadata
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Skip items that are not visible, or have no determined rect
|
||||
match item.rect_opt.get() {
|
||||
Some(rect) if rect.intersects(&visible_rect) => {}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
let Some(path) = item.path_opt().cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
struct ChildrenWrapper(PathBuf);
|
||||
impl Hash for ChildrenWrapper {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions.push(Subscription::run_with(
|
||||
ChildrenWrapper(path),
|
||||
|ChildrenWrapper(path)| {
|
||||
let path = path.clone();
|
||||
stream::channel(
|
||||
1,
|
||||
move |mut output: futures::channel::mpsc::Sender<_>| async move {
|
||||
let message = {
|
||||
let path = path.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let children = match fs::read_dir(&path) {
|
||||
Ok(entries) => entries.count(),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to read directory {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
0
|
||||
}
|
||||
};
|
||||
Message::DirectoryChildren(path, children)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
if let Err(err) = output.send(message).await {
|
||||
log::warn!("failed to send directory children: {err}");
|
||||
}
|
||||
|
||||
std::future::pending().await
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
for item in items {
|
||||
if item.thumbnail_opt.is_some() {
|
||||
// Skip items that already have a mime type and thumbnail
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue