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