Merge branch 'pop-os:master' into master
This commit is contained in:
commit
e10e73be69
15 changed files with 785 additions and 253 deletions
31
examples/gio-list.rs
Normal file
31
examples/gio-list.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use gio::prelude::*;
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let uri = env::args().nth(1).expect("no uri provided");
|
||||
let file = gio::File::for_uri(&uri);
|
||||
for entry_res in file
|
||||
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
|
||||
.unwrap()
|
||||
{
|
||||
let entry = entry_res.unwrap();
|
||||
println!("{:?}", entry.display_name());
|
||||
for attribute in entry.list_attributes(None) {
|
||||
println!(
|
||||
" {:?}: {:?}",
|
||||
attribute,
|
||||
entry.attribute_as_string(&attribute)
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: what is the best way to resolve shortcuts?
|
||||
let child = if let Some(target_uri) =
|
||||
entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
|
||||
{
|
||||
gio::File::for_uri(&target_uri)
|
||||
} else {
|
||||
file.child(entry.name())
|
||||
};
|
||||
println!("{:?}", child.uri());
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ notification-in-progress = File operations are in progress.
|
|||
trash = Trash
|
||||
recents = Recents
|
||||
undo = Undo
|
||||
today = Today
|
||||
|
||||
# List view
|
||||
name = Name
|
||||
|
|
@ -235,4 +236,4 @@ sort-z-a = Z-A
|
|||
sort-newest-first = Newest first
|
||||
sort-oldest-first = Oldest first
|
||||
sort-smallest-to-largest = Smallest to largest
|
||||
sort-largest-to-smallest = Largest to smallest
|
||||
sort-largest-to-smallest = Largest to smallest
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ empty-folder-hidden = Dossier vide (contient des éléments cachés)
|
|||
no-results = Aucun résultat trouvé
|
||||
filesystem = Système de fichiers
|
||||
home = Dossier personnel
|
||||
notification-in-progress = Les opérations sur les fichiers sont en cours.
|
||||
notification-in-progress = Des opérations sur des fichiers sont en cours.
|
||||
trash = Corbeille
|
||||
recents = Récente
|
||||
recents = Récents
|
||||
undo = Annuler
|
||||
|
||||
# List view
|
||||
|
|
@ -77,6 +77,31 @@ execute = Exécution
|
|||
## About
|
||||
git-description = Git commit {$hash} le {$date}
|
||||
|
||||
## Add Network Drive
|
||||
add-network-drive = Ajouter un lecteur réseau
|
||||
connect = Connecter
|
||||
connect-anonymously = Connecter anonymement
|
||||
connecting = Connection en cours...
|
||||
domain = Domaine
|
||||
enter-server-address = Entrez l'adresse du serveur
|
||||
network-drive-description =
|
||||
Les adresses de serveur incluent un préfixe de protocole et une adresse.
|
||||
Exemples: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
### Make sure to keep the comma which separates the columns
|
||||
network-drive-schemes =
|
||||
Protocoles disponibles,Préfixe
|
||||
AppleTalk,afp://
|
||||
File Transfer Protocol,ftp:// or ftps://
|
||||
Network File System,nfs://
|
||||
Server Message Block,smb://
|
||||
SSH File Transfer Protocol,sftp:// or ssh://
|
||||
WebDav,dav:// or davs://
|
||||
network-drive-error = Impossible d'accéder au lecteur réseau
|
||||
password = Mot de passe
|
||||
remember-password = Se souvenir du mot de passe
|
||||
try-again = Essayer à nouveau
|
||||
username = Nom d'utilisateur
|
||||
|
||||
## Operations
|
||||
edit-history = Modifier l'historique
|
||||
history = Historique
|
||||
|
|
@ -203,3 +228,12 @@ show-hidden-files = Afficher les fichiers cachés
|
|||
list-directories-first = Lister les répertoires en premier
|
||||
menu-settings = Paramètres...
|
||||
menu-about = À propos de Fichiers COSMIC...
|
||||
|
||||
## Sort
|
||||
sort = Trier
|
||||
sort-a-z = A-Z
|
||||
sort-z-a = Z-A
|
||||
sort-newest-first = Le plus récent en premier
|
||||
sort-oldest-first = Le plus ancien en premier
|
||||
sort-smallest-to-largest = Du plus petit au plus grand
|
||||
sort-largest-to-smallest = Du plus grand au plus petit
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ empty-folder-hidden = Pusty katalog (z ukrytymi plikami)
|
|||
no-results = Brak wyników
|
||||
filesystem = System plików
|
||||
home = Katalog Domowy
|
||||
networks = sieci
|
||||
notification-in-progress = Operacje na plikach w toku.
|
||||
trash = Kosz
|
||||
recents = Ubiegłe
|
||||
undo = Cofnij
|
||||
|
||||
# List view
|
||||
|
|
@ -62,11 +64,44 @@ apply-to-all = Zastosuj do wszystkich
|
|||
keep-both = Zachowaj oba
|
||||
skip = Pomiń
|
||||
|
||||
## Metadata Dialog
|
||||
owner = Właściciel
|
||||
group = Grupa
|
||||
other = Inni
|
||||
read = Odczyt
|
||||
write = Zapis
|
||||
execute = Wykonywanie
|
||||
|
||||
# Context Pages
|
||||
|
||||
## About
|
||||
git-description = Git commit {$hash} z {$date}
|
||||
|
||||
## Add Network Drive
|
||||
add-network-drive = Dodaj dysk sieciowy
|
||||
connect = Połącz
|
||||
connect-anonymously = Połącz anonimowo
|
||||
connecting = Łączenie...
|
||||
domain = Domena
|
||||
enter-server-address = Wprowadź adres serwera
|
||||
network-drive-description =
|
||||
Adres serwera zawiera prefiks protokołu i adres.
|
||||
Przykładowo: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
### Make sure to keep the comma which separates the columns
|
||||
network-drive-schemes =
|
||||
Dostępne protokoły,Prefiks
|
||||
AppleTalk,afp://
|
||||
File Transfer Protocol,ftp:// or ftps://
|
||||
Network File System,nfs://
|
||||
Server Message Block,smb://
|
||||
SSH File Transfer Protocol,sftp:// or ssh://
|
||||
WebDav,dav:// or davs://
|
||||
network-drive-error = Brak dostępu do dysku sieciowego
|
||||
password = Hasło
|
||||
remember-password = Zapamiętaj hasło
|
||||
try-again = Spróbuj ponownie
|
||||
username = Nazwa użytkownika
|
||||
|
||||
## Operations
|
||||
edit-history = Edytuj historię
|
||||
history = Historia
|
||||
|
|
@ -200,11 +235,6 @@ show-hidden-files = Pokaż ukryte pliki
|
|||
list-directories-first = Najpierw wyświetlaj katalogi
|
||||
menu-settings = Ustawienia...
|
||||
menu-about = O Plikach COSMIC...
|
||||
list-view = Widok listy
|
||||
show-hidden-files = Pokaż ukryte pliki
|
||||
list-directories-first = Najpierw wyświetlaj katalogi
|
||||
menu-settings = Ustawienia...
|
||||
menu-about = O Plikach COSMIC...
|
||||
|
||||
## Sort
|
||||
sort = Uszereguj
|
||||
|
|
@ -212,5 +242,5 @@ sort-a-z = A-Z
|
|||
sort-z-a = Z-A
|
||||
sort-newest-first = Najpierw najnowsze
|
||||
sort-oldest-first = Najpierw najstarsze
|
||||
sort-smallest-to-largest = Od najmniejszego do największego
|
||||
sort-largest-to-smallest = Od największego do najmniejszego
|
||||
sort-smallest-to-largest = Najpierw najmniejsze
|
||||
sort-largest-to-smallest = Najpierw największe
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ empty-folder = Tom katalog
|
|||
empty-folder-hidden = Tom katalog (har dolda objekt)
|
||||
filesystem = Filsystem
|
||||
home = Hem
|
||||
networks = Nätverk
|
||||
trash = Papperskorg
|
||||
recents = Senaste
|
||||
undo = Ångra
|
||||
today = Idag
|
||||
|
||||
# Dialog
|
||||
cancel = Avbryt
|
||||
|
|
@ -27,6 +31,15 @@ properties = Egenskaper
|
|||
|
||||
## Settings
|
||||
settings = Inställningar
|
||||
settings-tab = Flik
|
||||
settings-show-hidden = Visa dolda filer
|
||||
default-view = Standardvy
|
||||
icon-size-list = Ikonstorlek (lista)
|
||||
icon-size-grid = Ikonstorlek (rutnät)
|
||||
sorting-name = Sortera efter
|
||||
direction = Riktning
|
||||
ascending = Stigande
|
||||
descending = Fallande
|
||||
|
||||
### Appearance
|
||||
appearance = Utseende
|
||||
|
|
@ -78,3 +91,39 @@ default-app = {$name} (standard)
|
|||
|
||||
## Show details
|
||||
show-details = Visa detaljer
|
||||
|
||||
|
||||
network-drive-description =
|
||||
Serveradresser inkluderar ett protokollprefix och en adress.
|
||||
Exempel: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
### Se till att behålla kommatecken som skiljer kolumnerna åt
|
||||
network-drive-schemes =
|
||||
Tillgängliga protokoll, Prefix
|
||||
AppleTalk,afp://
|
||||
Filöverföringsprotokoll,ftp:// eller ftps://
|
||||
Nätverksfilsystem,nfs://
|
||||
Servermeddelandeblock,smb://
|
||||
SSH-filöverföringsprotokoll,sftp:// eller ssh://
|
||||
WebDav,dav:// eller davs://
|
||||
network-drive-error = Kan inte komma åt nätverksenheten
|
||||
password = Lösenord
|
||||
remember-password = Kom ihåg lösenord
|
||||
|
||||
## Lägg till en Nätverksenhet
|
||||
add-network-drive = Lägg till en Nätverksenhet
|
||||
connect = Anslut
|
||||
connect-anonymously = Anslut anonymt
|
||||
connecting = Ansluter...
|
||||
domain = Domän
|
||||
enter-server-address = Ange server address
|
||||
try-again = Försök igen
|
||||
username = Användarnamn
|
||||
|
||||
## Sort
|
||||
sort = Sortera
|
||||
sort-a-z = A-Z
|
||||
sort-z-a = Z-A
|
||||
sort-newest-first = Nyaste först
|
||||
sort-oldest-first = Äldst först
|
||||
sort-smallest-to-largest = Minsta till största
|
||||
sort-largest-to-smallest = Största till minsta
|
||||
|
|
|
|||
89
src/app.rs
89
src/app.rs
|
|
@ -521,11 +521,14 @@ impl App {
|
|||
location: Location,
|
||||
selection_path: Option<PathBuf>,
|
||||
) -> Command<Message> {
|
||||
let mounters = self.mounters.clone();
|
||||
let icon_sizes = self.config.tab.icon_sizes;
|
||||
Command::perform(
|
||||
async move {
|
||||
let location2 = location.clone();
|
||||
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
||||
match tokio::task::spawn_blocking(move || location2.scan(mounters, icon_sizes))
|
||||
.await
|
||||
{
|
||||
Ok(items) => {
|
||||
message::app(Message::TabRescan(entity, location, items, selection_path))
|
||||
}
|
||||
|
|
@ -591,7 +594,7 @@ impl App {
|
|||
if let Some(ref items) = tab.items_opt() {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
if let Some(path) = &item.path_opt {
|
||||
if let Some(Location::Path(path)) = &item.location_opt {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -672,7 +675,10 @@ impl App {
|
|||
.size(16)
|
||||
.handle(),
|
||||
))
|
||||
.data(Location::Networks)
|
||||
.data(Location::Network(
|
||||
"network:///".to_string(),
|
||||
fl!("networks"),
|
||||
))
|
||||
.divider_above()
|
||||
});
|
||||
|
||||
|
|
@ -941,49 +947,35 @@ impl App {
|
|||
fn properties(&self, entity: Option<ContextItem>) -> Element<Message> {
|
||||
match entity {
|
||||
None => self.tab_properties(self.tab_model.active()),
|
||||
|
||||
Some(ContextItem::TabBar(entity)) => self.tab_properties(entity),
|
||||
|
||||
Some(ContextItem::NavBar(item)) => {
|
||||
let mut children = Vec::new();
|
||||
|
||||
let mut children = Vec::with_capacity(1);
|
||||
if let Some(location) = self.nav_model.data::<Location>(item) {
|
||||
if let Location::Path(path) = location {
|
||||
let parent = path.parent().unwrap_or(path);
|
||||
|
||||
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
|
||||
if item.path_opt.as_deref() == Some(path) {
|
||||
children.push(item.property_view(IconSizes::default()));
|
||||
}
|
||||
//TODO: this should be done once, not when generating the view!
|
||||
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
|
||||
children.push(item.property_view(IconSizes::default()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
widget::settings::view_column(children).into()
|
||||
}
|
||||
|
||||
Some(ContextItem::BreadCrumbs(index)) => {
|
||||
let mut children = Vec::new();
|
||||
|
||||
let mut children = Vec::with_capacity(1);
|
||||
if let Some(tab) = self.tab_model.active_data::<Tab>() {
|
||||
let path = match tab.location {
|
||||
Location::Path(ref path) => Some(path),
|
||||
Location::Search(ref path, _) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
.and_then(|path| path.ancestors().nth(index))
|
||||
.map(|path| path.to_path_buf());
|
||||
if let Some(ref path) = path {
|
||||
let parent = path.parent().unwrap_or(path);
|
||||
|
||||
for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) {
|
||||
if item.path_opt.as_deref() == Some(path) {
|
||||
children.push(item.property_view(IconSizes::default()));
|
||||
}
|
||||
let path_opt = tab
|
||||
.location
|
||||
.path_opt()
|
||||
.and_then(|path| path.ancestors().nth(index))
|
||||
.map(|path| path.to_path_buf());
|
||||
if let Some(ref path) = path_opt {
|
||||
//TODO: this should be done once, not when generating the view!
|
||||
if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) {
|
||||
children.push(item.property_view(IconSizes::default()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
widget::settings::view_column(children).into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1772,9 +1764,7 @@ impl Application for App {
|
|||
//TODO: this could be further optimized by looking at what exactly changed
|
||||
if let Some(items) = &mut tab.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
if item.path_opt.as_ref()
|
||||
== Some(event_path)
|
||||
{
|
||||
if item.path_opt() == Some(event_path) {
|
||||
//TODO: reload more, like mime types?
|
||||
match fs::metadata(&event_path) {
|
||||
Ok(new_metadata) => match &mut item
|
||||
|
|
@ -1836,7 +1826,7 @@ impl Application for App {
|
|||
if let Some(items) = tab.items_opt() {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
if let Some(path) = &item.path_opt {
|
||||
if let Some(Location::Path(path)) = &item.location_opt {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2036,7 +2026,7 @@ impl Application for App {
|
|||
let mut selected = Vec::new();
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
if let Some(path) = &item.path_opt {
|
||||
if let Some(Location::Path(path)) = &item.location_opt {
|
||||
selected.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2398,11 +2388,14 @@ impl Application for App {
|
|||
self.toasts.remove(id);
|
||||
|
||||
let mut paths = Vec::with_capacity(recently_trashed.len());
|
||||
let mounters = self.mounters.clone();
|
||||
let icon_sizes = self.config.tab.icon_sizes;
|
||||
|
||||
return cosmic::command::future(async move {
|
||||
match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes))
|
||||
.await
|
||||
match tokio::task::spawn_blocking(move || {
|
||||
Location::Trash.scan(mounters, icon_sizes)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(items) => {
|
||||
for path in &*recently_trashed {
|
||||
|
|
@ -3551,6 +3544,7 @@ pub(crate) mod test_utils {
|
|||
|
||||
use crate::{
|
||||
config::{IconSizes, TabConfig},
|
||||
mounter::MounterMap,
|
||||
tab::Item,
|
||||
};
|
||||
|
||||
|
|
@ -3713,7 +3707,7 @@ pub(crate) mod test_utils {
|
|||
|
||||
// New tab with items
|
||||
let location = Location::Path(path.to_owned());
|
||||
let items = location.scan(IconSizes::default());
|
||||
let items = location.scan(Mounters::new(MounterMap::new()), IconSizes::default());
|
||||
let mut tab = Tab::new(location, TabConfig::default());
|
||||
tab.set_items(items);
|
||||
|
||||
|
|
@ -3748,7 +3742,7 @@ pub(crate) mod test_utils {
|
|||
|
||||
name == item.name
|
||||
&& is_dir == item.metadata.is_dir()
|
||||
&& path == item.path_opt.as_ref().expect("item should have path")
|
||||
&& path == item.path_opt().expect("item should have path")
|
||||
&& is_hidden == item.hidden
|
||||
}
|
||||
|
||||
|
|
@ -3768,6 +3762,19 @@ pub(crate) mod test_utils {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn assert_zoom_affects_item_size(tab: &mut Tab, message: tab::Message, should_zoom: bool) {
|
||||
let grid_icon_size = tab.config.icon_sizes.grid;
|
||||
let list_icon_size = tab.config.icon_sizes.list;
|
||||
|
||||
debug!("Emitting {:?}", message);
|
||||
tab.update(message, Modifiers::empty());
|
||||
|
||||
let grid_size_changed = grid_icon_size != tab.config.icon_sizes.grid;
|
||||
let list_size_changed = list_icon_size != tab.config.icon_sizes.list;
|
||||
|
||||
assert_eq!(grid_size_changed || list_size_changed, should_zoom);
|
||||
}
|
||||
|
||||
/// Assert that tab's items are equal to a path's entries.
|
||||
pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) {
|
||||
let Location::Path(ref tab_path) = tab.location else {
|
||||
|
|
|
|||
|
|
@ -376,10 +376,12 @@ struct App {
|
|||
impl App {
|
||||
fn rescan_tab(&self) -> Command<Message> {
|
||||
let location = self.tab.location.clone();
|
||||
let mounters = self.mounters.clone();
|
||||
let icon_sizes = self.tab.config.icon_sizes;
|
||||
Command::perform(
|
||||
async move {
|
||||
match tokio::task::spawn_blocking(move || location.scan(icon_sizes)).await {
|
||||
match tokio::task::spawn_blocking(move || location.scan(mounters, icon_sizes)).await
|
||||
{
|
||||
Ok(items) => message::app(Message::TabRescan(items)),
|
||||
Err(err) => {
|
||||
log::warn!("failed to rescan: {}", err);
|
||||
|
|
@ -895,7 +897,7 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
DialogPage::Replace { filename } => {
|
||||
DialogPage::Replace { .. } => {
|
||||
return self.update(Message::Save(true));
|
||||
}
|
||||
}
|
||||
|
|
@ -999,7 +1001,7 @@ impl Application for App {
|
|||
//TODO: this could be further optimized by looking at what exactly changed
|
||||
if let Some(items) = &mut self.tab.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
if item.path_opt.as_ref() == Some(event_path) {
|
||||
if item.path_opt() == Some(event_path) {
|
||||
//TODO: reload more, like mime types?
|
||||
match fs::metadata(&event_path) {
|
||||
Ok(new_metadata) => {
|
||||
|
|
@ -1049,7 +1051,7 @@ impl Application for App {
|
|||
if let Some(items) = self.tab.items_opt() {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
if let Some(path) = &item.path_opt {
|
||||
if let Some(Location::Path(path)) = &item.location_opt {
|
||||
paths.push(path.clone());
|
||||
let _ = update_recently_used(
|
||||
&path.clone(),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ mod spawn_detached;
|
|||
use tab::Location;
|
||||
pub mod tab;
|
||||
|
||||
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||
err.to_string()
|
||||
}
|
||||
|
||||
pub fn home_dir() -> PathBuf {
|
||||
match dirs::home_dir() {
|
||||
Some(home) => home,
|
||||
|
|
|
|||
18
src/menu.rs
18
src/menu.rs
|
|
@ -197,8 +197,22 @@ pub fn context_menu<'a>(
|
|||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
(_, Location::Networks) => {
|
||||
//TODO: networks context menu?
|
||||
(_, Location::Network(_, _)) => {
|
||||
if selected > 0 {
|
||||
if selected_dir == 1 && selected == 1 || selected_dir == 0 {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
}
|
||||
} else {
|
||||
if tab.mode.multiple() {
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
}
|
||||
if !children.is_empty() {
|
||||
children.push(divider::horizontal::light().into());
|
||||
}
|
||||
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
|
||||
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
|
||||
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
|
||||
}
|
||||
}
|
||||
(_, Location::Trash) => {
|
||||
if tab.mode.multiple() {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ use cosmic::{
|
|||
widget, Command,
|
||||
};
|
||||
use gio::{glib, prelude::*};
|
||||
use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc};
|
||||
use std::{any::TypeId, cell::Cell, future::pending, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||
use crate::{
|
||||
config::IconSizes,
|
||||
err_str,
|
||||
tab::{self, ItemMetadata, ItemThumbnail, Location},
|
||||
};
|
||||
|
||||
fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
||||
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
||||
|
|
@ -21,10 +26,154 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option<PathBuf> {
|
|||
None
|
||||
}
|
||||
|
||||
fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
||||
let file = gio::File::for_uri(uri);
|
||||
let mut items = Vec::new();
|
||||
for info_res in file
|
||||
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)
|
||||
.map_err(err_str)?
|
||||
{
|
||||
let info = info_res.map_err(err_str)?;
|
||||
println!("{:?}", info.display_name());
|
||||
for attribute in info.list_attributes(None) {
|
||||
println!(
|
||||
" {:?}: {:?}: {:?}",
|
||||
attribute,
|
||||
info.attribute_type(&attribute),
|
||||
info.attribute_as_string(&attribute)
|
||||
);
|
||||
}
|
||||
|
||||
let name = info.name().to_string_lossy().to_string();
|
||||
let display_name = info.display_name().to_string();
|
||||
|
||||
//TODO: what is the best way to resolve shortcuts?
|
||||
let location = Location::Network(
|
||||
if let Some(target_uri) = info.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI)
|
||||
{
|
||||
target_uri.to_string()
|
||||
} else {
|
||||
file.child(info.name()).uri().to_string()
|
||||
},
|
||||
display_name.clone(),
|
||||
);
|
||||
|
||||
//TODO: support dir or file
|
||||
let metadata = ItemMetadata::SimpleDir { entries: 0 };
|
||||
|
||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||
let file_icon = |size| {
|
||||
info.icon()
|
||||
.as_ref()
|
||||
.and_then(|icon| gio_icon_to_path(icon, size))
|
||||
.map(|path| widget::icon::from_path(path))
|
||||
.unwrap_or(
|
||||
widget::icon::from_name(if metadata.is_dir() {
|
||||
"folder"
|
||||
} else {
|
||||
"text-x-generic"
|
||||
})
|
||||
.size(size)
|
||||
.handle(),
|
||||
)
|
||||
};
|
||||
(
|
||||
//TODO: get mime from content_type?
|
||||
"inode/directory".parse().unwrap(),
|
||||
file_icon(sizes.grid()),
|
||||
file_icon(sizes.list()),
|
||||
file_icon(sizes.list_condensed()),
|
||||
)
|
||||
};
|
||||
|
||||
items.push(tab::Item {
|
||||
name,
|
||||
display_name,
|
||||
metadata,
|
||||
hidden: false,
|
||||
location_opt: Some(location),
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
icon_handle_list_condensed,
|
||||
open_with: Vec::new(),
|
||||
thumbnail_opt: Some(ItemThumbnail::NotImage),
|
||||
button_id: widget::Id::unique(),
|
||||
pos_opt: Cell::new(None),
|
||||
rect_opt: Cell::new(None),
|
||||
selected: false,
|
||||
overlaps_drag_rect: false,
|
||||
});
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
|
||||
let mount_op = gio::MountOperation::new();
|
||||
mount_op.connect_ask_password(
|
||||
move |mount_op, message, default_user, default_domain, flags| {
|
||||
let auth = MounterAuth {
|
||||
message: message.to_string(),
|
||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
||||
Some(default_user.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
||||
Some(default_domain.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
||||
Some(String::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||
event_tx
|
||||
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
|
||||
.unwrap();
|
||||
//TODO: async recv?
|
||||
if let Some(auth) = auth_rx.blocking_recv() {
|
||||
if auth.anonymous_opt == Some(true) {
|
||||
mount_op.set_anonymous(true);
|
||||
} else {
|
||||
mount_op.set_username(auth.username_opt.as_deref());
|
||||
mount_op.set_domain(auth.domain_opt.as_deref());
|
||||
mount_op.set_password(auth.password_opt.as_deref());
|
||||
if auth.remember_opt == Some(true) {
|
||||
mount_op.set_password_save(gio::PasswordSave::Permanently);
|
||||
}
|
||||
}
|
||||
mount_op.reply(gio::MountOperationResult::Handled);
|
||||
} else {
|
||||
mount_op.reply(gio::MountOperationResult::Aborted);
|
||||
}
|
||||
},
|
||||
);
|
||||
mount_op
|
||||
}
|
||||
|
||||
enum Cmd {
|
||||
Rescan,
|
||||
Mount(MounterItem),
|
||||
NetworkDrive(String),
|
||||
NetworkScan(
|
||||
String,
|
||||
IconSizes,
|
||||
mpsc::Sender<Result<Vec<tab::Item>, String>>,
|
||||
),
|
||||
Unmount(MounterItem),
|
||||
}
|
||||
|
||||
|
|
@ -194,62 +343,8 @@ impl Gvfs {
|
|||
}
|
||||
}
|
||||
Cmd::NetworkDrive(uri) => {
|
||||
let mount_op = gio::MountOperation::new();
|
||||
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
let uri = uri.clone();
|
||||
mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| {
|
||||
let auth = MounterAuth {
|
||||
message: message.to_string(),
|
||||
username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) {
|
||||
Some(default_user.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) {
|
||||
Some(default_domain.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) {
|
||||
Some(String::new())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap();
|
||||
//TODO: async recv?
|
||||
if let Some(auth) = auth_rx.blocking_recv() {
|
||||
if auth.anonymous_opt == Some(true) {
|
||||
mount_op.set_anonymous(true);
|
||||
} else {
|
||||
mount_op.set_username(auth.username_opt.as_deref());
|
||||
mount_op.set_domain(auth.domain_opt.as_deref());
|
||||
mount_op.set_password(auth.password_opt.as_deref());
|
||||
if auth.remember_opt == Some(true) {
|
||||
mount_op.set_password_save(gio::PasswordSave::Permanently);
|
||||
}
|
||||
}
|
||||
mount_op.reply(gio::MountOperationResult::Handled);
|
||||
} else {
|
||||
mount_op.reply(gio::MountOperationResult::Aborted);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let file = gio::File::for_uri(&uri);
|
||||
let mount_op = mount_op(uri.clone(), event_tx.clone());
|
||||
let event_tx = event_tx.clone();
|
||||
file.mount_enclosing_volume(
|
||||
gio::MountMountFlags::empty(),
|
||||
|
|
@ -267,6 +362,40 @@ impl Gvfs {
|
|||
}
|
||||
);
|
||||
}
|
||||
Cmd::NetworkScan(uri, sizes, items_tx) => {
|
||||
let file = gio::File::for_uri(&uri);
|
||||
let needs_mount = match file.find_enclosing_mount(gio::Cancellable::NONE) {
|
||||
Ok(_) => false,
|
||||
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
||||
Some(gio::IOErrorEnum::NotMounted) => true,
|
||||
_ => false
|
||||
}
|
||||
};
|
||||
if needs_mount {
|
||||
let mount_op = mount_op(uri.clone(), event_tx.clone());
|
||||
let event_tx = event_tx.clone();
|
||||
file.mount_enclosing_volume(
|
||||
gio::MountMountFlags::empty(),
|
||||
Some(&mount_op),
|
||||
gio::Cancellable::NONE,
|
||||
move |res| {
|
||||
log::info!("network scan mounted {}: result {:?}", uri, res);
|
||||
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
|
||||
event_tx.send(Event::NetworkResult(uri, match res {
|
||||
Ok(()) => {
|
||||
Ok(true)
|
||||
},
|
||||
Err(err) => match err.kind::<gio::IOErrorEnum>() {
|
||||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||
_ => Err(format!("{}", err))
|
||||
}
|
||||
})).unwrap();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
items_tx.send(network_scan(&uri, sizes)).await.unwrap();
|
||||
}
|
||||
}
|
||||
Cmd::Unmount(mounter_item) => {
|
||||
let MounterItem::Gvfs(item) = mounter_item else { continue };
|
||||
let ItemKind::Mount = item.kind else { continue };
|
||||
|
|
@ -330,6 +459,14 @@ impl Mounter for Gvfs {
|
|||
)
|
||||
}
|
||||
|
||||
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>> {
|
||||
let (items_tx, mut items_rx) = mpsc::channel(1);
|
||||
self.command_tx
|
||||
.send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx))
|
||||
.unwrap();
|
||||
items_rx.blocking_recv()
|
||||
}
|
||||
|
||||
fn unmount(&self, item: MounterItem) -> Command<()> {
|
||||
let command_tx = self.command_tx.clone();
|
||||
Command::perform(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use cosmic::{iced::subscription, widget, Command};
|
|||
use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{config::IconSizes, tab};
|
||||
|
||||
#[cfg(feature = "gvfs")]
|
||||
mod gvfs;
|
||||
|
||||
|
|
@ -86,10 +88,11 @@ pub enum MounterMessage {
|
|||
NetworkResult(String, Result<bool, String>),
|
||||
}
|
||||
|
||||
pub trait Mounter {
|
||||
pub trait Mounter: Send + Sync {
|
||||
//TODO: send result
|
||||
fn mount(&self, item: MounterItem) -> Command<()>;
|
||||
fn network_drive(&self, uri: String) -> Command<()>;
|
||||
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
|
||||
fn unmount(&self, item: MounterItem) -> Command<()>;
|
||||
fn subscription(&self) -> subscription::Subscription<MounterMessage>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,14 @@ use cosmic::{
|
|||
iced_core::{
|
||||
border::Border,
|
||||
event::{self, Event},
|
||||
keyboard::{
|
||||
self,
|
||||
key::{self, Key},
|
||||
Event::{KeyPressed, KeyReleased},
|
||||
Modifiers,
|
||||
},
|
||||
layout,
|
||||
mouse::{self, click},
|
||||
mouse::{self, click, Event as MouseEvent},
|
||||
overlay,
|
||||
renderer::{self, Quad, Renderer as _},
|
||||
touch,
|
||||
|
|
@ -39,6 +45,7 @@ pub struct MouseArea<'a, Message> {
|
|||
on_back_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||
on_forward_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||
on_forward_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a>>,
|
||||
show_drag_rect: bool,
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +151,16 @@ impl<'a, Message> MouseArea<'a, Message> {
|
|||
self
|
||||
}
|
||||
|
||||
/// The message to emit on a scroll.
|
||||
#[must_use]
|
||||
pub fn on_scroll(
|
||||
mut self,
|
||||
message: impl Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a,
|
||||
) -> Self {
|
||||
self.on_scroll = Some(Box::new(message));
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self {
|
||||
self.show_drag_rect = show_drag_rect;
|
||||
|
|
@ -163,7 +180,7 @@ impl<'a, Message> MouseArea<'a, Message> {
|
|||
struct State {
|
||||
// TODO: Support on_mouse_enter and on_mouse_exit
|
||||
drag_initiated: Option<Point>,
|
||||
|
||||
modifiers: Modifiers,
|
||||
prev_click: Option<(mouse::Click, Instant)>,
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +244,7 @@ impl<'a, Message> MouseArea<'a, Message> {
|
|||
on_back_release: None,
|
||||
on_forward_press: None,
|
||||
on_forward_release: None,
|
||||
on_scroll: None,
|
||||
show_drag_rect: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -578,6 +596,21 @@ fn update<Message: Clone>(
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_scroll.as_ref() {
|
||||
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
|
||||
if let Some(on_scroll) = widget.on_scroll.as_ref() {
|
||||
if let Some(message) = on_scroll(delta.clone(), state.modifiers) {
|
||||
shell.publish(message);
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Event::Keyboard(key_event) = event {
|
||||
handle_key_event(key_event, state)
|
||||
};
|
||||
|
||||
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
|
||||
shell.publish(message(drag_rect.intersection(&layout_bounds).map(
|
||||
|mut rect| {
|
||||
|
|
@ -590,3 +623,53 @@ fn update<Message: Clone>(
|
|||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn handle_key_event(key_event: &keyboard::Event, state: &mut State) {
|
||||
if let KeyPressed {
|
||||
key: Key::Named(key::Named::Control),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.insert(Modifiers::CTRL);
|
||||
}
|
||||
|
||||
if let KeyReleased {
|
||||
key: Key::Named(key::Named::Control),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.remove(Modifiers::CTRL);
|
||||
}
|
||||
|
||||
if let KeyPressed {
|
||||
key: Key::Named(key::Named::Shift),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.insert(Modifiers::SHIFT);
|
||||
}
|
||||
|
||||
if let KeyReleased {
|
||||
key: Key::Named(key::Named::Shift),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.remove(Modifiers::SHIFT);
|
||||
}
|
||||
|
||||
if let KeyPressed {
|
||||
key: Key::Named(key::Named::Alt),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.insert(Modifiers::ALT);
|
||||
}
|
||||
|
||||
if let KeyReleased {
|
||||
key: Key::Named(key::Named::Alt),
|
||||
..
|
||||
} = key_event
|
||||
{
|
||||
state.modifiers.remove(Modifiers::ALT);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,11 @@ use walkdir::WalkDir;
|
|||
use crate::{
|
||||
app::{ArchiveType, DialogPage, Message},
|
||||
config::IconSizes,
|
||||
fl,
|
||||
err_str, fl,
|
||||
mime_icon::mime_for_path,
|
||||
tab,
|
||||
};
|
||||
|
||||
fn err_str<T: ToString>(err: T) -> String {
|
||||
err.to_string()
|
||||
}
|
||||
|
||||
fn handle_replace(
|
||||
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
||||
file_from: PathBuf,
|
||||
|
|
|
|||
405
src/tab.rs
405
src/tab.rs
|
|
@ -7,8 +7,9 @@ use cosmic::{
|
|||
},
|
||||
alignment::{Horizontal, Vertical},
|
||||
clipboard::dnd::DndAction,
|
||||
event,
|
||||
futures::SinkExt,
|
||||
keyboard::Modifiers,
|
||||
keyboard::{self, Modifiers},
|
||||
subscription::{self, Subscription},
|
||||
//TODO: export in cosmic::widget
|
||||
widget::{
|
||||
|
|
@ -24,7 +25,7 @@ use cosmic::{
|
|||
Rectangle,
|
||||
Size,
|
||||
},
|
||||
iced_core::widget::tree,
|
||||
iced_core::{mouse::ScrollDelta, widget::tree},
|
||||
iced_style::rule,
|
||||
theme,
|
||||
widget::{
|
||||
|
|
@ -44,7 +45,7 @@ use std::{
|
|||
cell::{Cell, RefCell},
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
fmt::{self, Display},
|
||||
fs::{self, Metadata},
|
||||
num::NonZeroU16,
|
||||
os::unix::fs::MetadataExt,
|
||||
|
|
@ -63,6 +64,7 @@ use crate::{
|
|||
menu,
|
||||
mime_app::{mime_apps, MimeApp},
|
||||
mime_icon::{mime_for_path, mime_icon},
|
||||
mounter::Mounters,
|
||||
mouse_area,
|
||||
};
|
||||
use unix_permissions_ext::UNIXPermissionsExt;
|
||||
|
|
@ -72,7 +74,8 @@ pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
|
|||
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
||||
|
||||
//TODO: adjust for locales?
|
||||
const TIME_FORMAT: &'static str = "%a %-d %b %-Y %r";
|
||||
const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p";
|
||||
const TIME_FORMAT: &'static str = "%-I:%M %p";
|
||||
static SPECIAL_DIRS: Lazy<HashMap<PathBuf, &'static str>> = Lazy::new(|| {
|
||||
let mut special_dirs = HashMap::new();
|
||||
if let Some(dir) = dirs::document_dir() {
|
||||
|
|
@ -260,6 +263,31 @@ fn format_permissions(metadata: &Metadata, owner: PermissionOwner) -> String {
|
|||
perms.join(" ")
|
||||
}
|
||||
|
||||
struct FormatTime(std::time::SystemTime);
|
||||
|
||||
impl Display for FormatTime {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let date_time = chrono::DateTime::<chrono::Local>::from(self.0);
|
||||
let now = chrono::Local::now();
|
||||
if date_time.date() == now.date() {
|
||||
write!(
|
||||
f,
|
||||
"{}, {}",
|
||||
fl!("today"),
|
||||
date_time.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
)
|
||||
} else {
|
||||
date_time
|
||||
.format_localized(DATE_TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(time: std::time::SystemTime) -> FormatTime {
|
||||
FormatTime(time)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn hidden_attribute(_metadata: &Metadata) -> bool {
|
||||
false
|
||||
|
|
@ -377,7 +405,7 @@ pub fn item_from_entry(
|
|||
display_name,
|
||||
metadata: ItemMetadata::Path { metadata, children },
|
||||
hidden,
|
||||
path_opt: Some(path),
|
||||
location_opt: Some(Location::Path(path)),
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -535,7 +563,7 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec<Item
|
|||
items.par_sort_unstable_by(|a, b| {
|
||||
let get_modified = |x: &Item| match &x.metadata {
|
||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||
ItemMetadata::Trash { .. } => None,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Sort with latest modified first
|
||||
|
|
@ -621,7 +649,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
|||
display_name,
|
||||
metadata: ItemMetadata::Trash { metadata, entry },
|
||||
hidden: false,
|
||||
path_opt: None,
|
||||
location_opt: None,
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -724,9 +752,17 @@ pub fn scan_recents(sizes: IconSizes) -> Vec<Item> {
|
|||
recents.into_iter().take(50).map(|(item, _)| item).collect()
|
||||
}
|
||||
|
||||
pub fn scan_networks(sizes: IconSizes) -> Vec<Item> {
|
||||
//TODO: network folder items
|
||||
vec![]
|
||||
pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
||||
for (key, mounter) in mounters.iter() {
|
||||
match mounter.network_scan(uri, sizes) {
|
||||
Some(Ok(items)) => return items,
|
||||
Some(Err(err)) => {
|
||||
log::warn!("failed to scan {:?}: {}", uri, err);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
|
@ -735,7 +771,7 @@ pub enum Location {
|
|||
Search(PathBuf, String),
|
||||
Trash,
|
||||
Recents,
|
||||
Networks,
|
||||
Network(String, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Location {
|
||||
|
|
@ -745,19 +781,27 @@ impl std::fmt::Display for Location {
|
|||
Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term),
|
||||
Self::Trash => write!(f, "trash"),
|
||||
Self::Recents => write!(f, "recents"),
|
||||
Self::Networks => write!(f, "networks"),
|
||||
Self::Network(uri, _) => write!(f, "{}", uri),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Location {
|
||||
pub fn scan(&self, sizes: IconSizes) -> Vec<Item> {
|
||||
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||
match self {
|
||||
Self::Path(path) => Some(&path),
|
||||
Self::Search(path, _) => Some(&path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec<Item> {
|
||||
match self {
|
||||
Self::Path(path) => scan_path(path, sizes),
|
||||
Self::Search(path, term) => scan_search(path, term, sizes),
|
||||
Self::Trash => scan_trash(sizes),
|
||||
Self::Recents => scan_recents(sizes),
|
||||
Self::Networks => scan_networks(sizes),
|
||||
Self::Network(uri, _) => scan_network(uri, mounters, sizes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -847,6 +891,12 @@ pub enum ItemMetadata {
|
|||
metadata: trash::TrashItemMetadata,
|
||||
entry: trash::TrashItem,
|
||||
},
|
||||
SimpleDir {
|
||||
entries: u64,
|
||||
},
|
||||
SimpleFile {
|
||||
size: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ItemMetadata {
|
||||
|
|
@ -857,6 +907,8 @@ impl ItemMetadata {
|
|||
trash::TrashItemSize::Entries(_) => true,
|
||||
trash::TrashItemSize::Bytes(_) => false,
|
||||
},
|
||||
Self::SimpleDir { .. } => true,
|
||||
Self::SimpleFile { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -874,7 +926,7 @@ pub struct Item {
|
|||
pub display_name: String,
|
||||
pub metadata: ItemMetadata,
|
||||
pub hidden: bool,
|
||||
pub path_opt: Option<PathBuf>,
|
||||
pub location_opt: Option<Location>,
|
||||
pub mime: Mime,
|
||||
pub icon_handle_grid: widget::icon::Handle,
|
||||
pub icon_handle_list: widget::icon::Handle,
|
||||
|
|
@ -894,6 +946,10 @@ impl Item {
|
|||
name.replace(".", ".\u{200B}").replace("_", "_\u{200B}")
|
||||
}
|
||||
|
||||
pub fn path_opt(&self) -> Option<&PathBuf> {
|
||||
self.location_opt.as_ref()?.path_opt()
|
||||
}
|
||||
|
||||
fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> {
|
||||
// This loads the image only if thumbnailing worked
|
||||
let icon = widget::icon::icon(self.icon_handle_grid.clone())
|
||||
|
|
@ -907,7 +963,7 @@ impl Item {
|
|||
{
|
||||
ItemThumbnail::NotImage => icon,
|
||||
ItemThumbnail::Rgba(_) => {
|
||||
if let Some(path) = &self.path_opt {
|
||||
if let Some(Location::Path(path)) = &self.location_opt {
|
||||
widget::image::viewer(widget::image::Handle::from_path(path))
|
||||
.min_scale(1.0)
|
||||
.into()
|
||||
|
|
@ -916,7 +972,7 @@ impl Item {
|
|||
}
|
||||
}
|
||||
ItemThumbnail::Svg => {
|
||||
if let Some(path) = &self.path_opt {
|
||||
if let Some(Location::Path(path)) = &self.location_opt {
|
||||
widget::Svg::from_path(path).into()
|
||||
} else {
|
||||
icon
|
||||
|
|
@ -944,7 +1000,7 @@ impl Item {
|
|||
|
||||
column = column.push(widget::text(format!("Type: {}", self.mime)));
|
||||
|
||||
if let Some(path) = &self.path_opt {
|
||||
if let Some(Location::Path(path)) = &self.location_opt {
|
||||
for app in self.open_with.iter() {
|
||||
column = column.push(
|
||||
widget::button(
|
||||
|
|
@ -998,27 +1054,15 @@ impl Item {
|
|||
}
|
||||
|
||||
if let Ok(time) = metadata.created() {
|
||||
column = column.push(widget::text(format!(
|
||||
"Created: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
)));
|
||||
column = column.push(widget::text(format!("Created: {}", format_time(time))));
|
||||
}
|
||||
|
||||
if let Ok(time) = metadata.modified() {
|
||||
column = column.push(widget::text(format!(
|
||||
"Modified: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
)));
|
||||
column = column.push(widget::text(format!("Modified: {}", format_time(time))));
|
||||
}
|
||||
|
||||
if let Ok(time) = metadata.accessed() {
|
||||
column = column.push(widget::text(format!(
|
||||
"Accessed: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
)));
|
||||
column = column.push(widget::text(format!("Accessed: {}", format_time(time))));
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
|
|
@ -1061,8 +1105,8 @@ impl Item {
|
|||
);
|
||||
}
|
||||
}
|
||||
ItemMetadata::Trash { .. } => {
|
||||
//TODO: trash metadata
|
||||
_ => {
|
||||
//TODO: other metadata types
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1097,13 +1141,12 @@ impl Item {
|
|||
if let Ok(time) = metadata.modified() {
|
||||
column = column.push(widget::text(format!(
|
||||
"Last modified: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
format_time(time)
|
||||
)));
|
||||
}
|
||||
}
|
||||
ItemMetadata::Trash { .. } => {
|
||||
//TODO: trash metadata
|
||||
_ => {
|
||||
//TODO: other metadata
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1253,9 +1296,7 @@ impl Tab {
|
|||
Location::Recents => {
|
||||
fl!("recents")
|
||||
}
|
||||
Location::Networks => {
|
||||
fl!("networks")
|
||||
}
|
||||
Location::Network(_uri, display_name) => display_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1309,10 +1350,11 @@ impl Tab {
|
|||
}
|
||||
|
||||
pub fn select_path(&mut self, path: PathBuf) {
|
||||
let location = Location::Path(path);
|
||||
*self.cached_selected.borrow_mut() = None;
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
item.selected = item.path_opt.as_ref() == Some(&path);
|
||||
item.selected = item.location_opt.as_ref() == Some(&location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1566,14 +1608,18 @@ impl Tab {
|
|||
.as_ref()
|
||||
.and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i)))
|
||||
{
|
||||
if let Some(path) = &clicked_item.path_opt {
|
||||
if let Some(location) = &clicked_item.location_opt {
|
||||
if clicked_item.metadata.is_dir() {
|
||||
cd = Some(Location::Path(path.clone()));
|
||||
cd = Some(location.clone());
|
||||
} else {
|
||||
commands.push(Command::OpenFile(path.clone()));
|
||||
if let Location::Path(path) = location {
|
||||
commands.push(Command::OpenFile(path.clone()));
|
||||
} else {
|
||||
log::warn!("no path for item {:?}", clicked_item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("no path for item {:?}", clicked_item);
|
||||
log::warn!("no location for item {:?}", clicked_item);
|
||||
}
|
||||
} else {
|
||||
log::warn!("no item for click index {:?}", click_i_opt);
|
||||
|
|
@ -1964,12 +2010,14 @@ impl Tab {
|
|||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
if let Some(path) = &item.path_opt {
|
||||
if path.is_dir() {
|
||||
if let Some(location) = &item.location_opt {
|
||||
if item.metadata.is_dir() {
|
||||
//TODO: allow opening multiple tabs?
|
||||
cd = Some(Location::Path(path.clone()));
|
||||
cd = Some(location.clone());
|
||||
} else {
|
||||
commands.push(Command::OpenFile(path.clone()));
|
||||
if let Location::Path(path) = location {
|
||||
commands.push(Command::OpenFile(path.clone()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//TODO: open properties?
|
||||
|
|
@ -2004,7 +2052,7 @@ impl Tab {
|
|||
if let Some(clicked_item) =
|
||||
self.items_opt.as_ref().and_then(|items| items.get(click_i))
|
||||
{
|
||||
if let Some(path) = &clicked_item.path_opt {
|
||||
if let Some(Location::Path(path)) = &clicked_item.location_opt {
|
||||
if clicked_item.metadata.is_dir() {
|
||||
//cd = Some(Location::Path(path.clone()));
|
||||
commands.push(Command::OpenInNewTab(path.clone()))
|
||||
|
|
@ -2035,8 +2083,9 @@ impl Tab {
|
|||
}
|
||||
Message::Thumbnail(path, thumbnail) => {
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
let location = Location::Path(path);
|
||||
for item in items.iter_mut() {
|
||||
if item.path_opt.as_ref() == Some(&path) {
|
||||
if item.location_opt.as_ref() == Some(&location) {
|
||||
if let ItemThumbnail::Rgba(rgba) = &thumbnail {
|
||||
//TODO: pass handles already generated to avoid blocking main thread
|
||||
let handle = widget::icon::from_raster_pixels(
|
||||
|
|
@ -2085,20 +2134,11 @@ impl Tab {
|
|||
}
|
||||
commands.push(Command::DropFiles(to, from))
|
||||
}
|
||||
Location::Search(_, _) => {
|
||||
log::warn!(" Copy/cut to search not supported.");
|
||||
}
|
||||
Location::Trash if matches!(from.kind, ClipboardKind::Cut) => {
|
||||
commands.push(Command::MoveToTrash(from.paths))
|
||||
}
|
||||
Location::Trash => {
|
||||
log::warn!("Copy to trash is not supported.");
|
||||
}
|
||||
Location::Recents => {
|
||||
log::warn!("Copy to recents is not supported.");
|
||||
}
|
||||
Location::Networks => {
|
||||
log::warn!("Copy to networks is not supported.");
|
||||
_ => {
|
||||
log::warn!("{:?} to {:?} is not supported.", from.kind, to);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -2227,6 +2267,8 @@ impl Tab {
|
|||
trash::TrashItemSize::Entries(entries) => (true, entries as u64),
|
||||
trash::TrashItemSize::Bytes(bytes) => (false, bytes),
|
||||
},
|
||||
ItemMetadata::SimpleDir { entries } => (true, *entries),
|
||||
ItemMetadata::SimpleFile { size } => (false, *size),
|
||||
};
|
||||
let (a_is_entry, a_size) = get_size(a.1);
|
||||
let (b_is_entry, b_size) = get_size(b.1);
|
||||
|
|
@ -2260,7 +2302,7 @@ impl Tab {
|
|||
items.sort_by(|a, b| {
|
||||
let get_modified = |x: &Item| match &x.metadata {
|
||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||
ItemMetadata::Trash { .. } => None,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let a_modified = get_modified(a.1);
|
||||
|
|
@ -2561,11 +2603,14 @@ impl Tab {
|
|||
.into(),
|
||||
);
|
||||
}
|
||||
Location::Networks => {
|
||||
Location::Network(uri, display_name) => {
|
||||
children.push(
|
||||
widget::button(widget::text::heading(fl!("networks")))
|
||||
widget::button(widget::text::heading(display_name))
|
||||
.padding(space_xxxs)
|
||||
.on_press(Message::Location(Location::Networks))
|
||||
.on_press(Message::Location(Location::Network(
|
||||
uri.clone(),
|
||||
display_name.clone(),
|
||||
)))
|
||||
.style(theme::Button::Text)
|
||||
.into(),
|
||||
);
|
||||
|
|
@ -2765,9 +2810,10 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
|
||||
let column: Element<Message> = if item.metadata.is_dir() && item.path_opt.is_some()
|
||||
let column: Element<Message> = if item.metadata.is_dir()
|
||||
&& item.location_opt.is_some()
|
||||
{
|
||||
let tab_location = Location::Path(item.path_opt.clone().unwrap());
|
||||
let tab_location = item.location_opt.clone().unwrap();
|
||||
let tab_location_enter = tab_location.clone();
|
||||
let tab_location_leave = tab_location.clone();
|
||||
let is_dnd_hovered =
|
||||
|
|
@ -3036,18 +3082,21 @@ impl Tab {
|
|||
|
||||
let modified_text = match &item.metadata {
|
||||
ItemMetadata::Path { metadata, .. } => match metadata.modified() {
|
||||
Ok(time) => chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO)
|
||||
.to_string(),
|
||||
Ok(time) => format_time(time).to_string(),
|
||||
Err(_) => String::new(),
|
||||
},
|
||||
ItemMetadata::Trash { .. } => String::new(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let size_text = match &item.metadata {
|
||||
ItemMetadata::Path { metadata, children } => {
|
||||
if metadata.is_dir() {
|
||||
format!("{} items", children)
|
||||
//TODO: translate
|
||||
if *children == 1 {
|
||||
format!("{} item", children)
|
||||
} else {
|
||||
format!("{} items", children)
|
||||
}
|
||||
} else {
|
||||
format_size(metadata.len())
|
||||
}
|
||||
|
|
@ -3063,6 +3112,15 @@ impl Tab {
|
|||
}
|
||||
trash::TrashItemSize::Bytes(bytes) => format_size(bytes),
|
||||
},
|
||||
ItemMetadata::SimpleDir { entries } => {
|
||||
//TODO: translate
|
||||
if *entries == 1 {
|
||||
format!("{} item", entries)
|
||||
} else {
|
||||
format!("{} items", entries)
|
||||
}
|
||||
}
|
||||
ItemMetadata::SimpleFile { size } => format_size(*size),
|
||||
};
|
||||
|
||||
let row = if condensed {
|
||||
|
|
@ -3126,58 +3184,59 @@ impl Tab {
|
|||
};
|
||||
|
||||
let button_row = button(row.into());
|
||||
let button_row: Element<_> = if item.metadata.is_dir() && item.path_opt.is_some() {
|
||||
let tab_location = Location::Path(item.path_opt.clone().unwrap());
|
||||
let tab_location_enter = tab_location.clone();
|
||||
let tab_location_leave = tab_location.clone();
|
||||
let is_dnd_hovered =
|
||||
self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location);
|
||||
cosmic::widget::container(
|
||||
DndDestination::for_data(button_row, move |data, action| {
|
||||
if let Some(mut data) = data {
|
||||
if action == DndAction::Copy {
|
||||
Message::Drop(Some((tab_location.clone(), data)))
|
||||
} else if action == DndAction::Move {
|
||||
data.kind = ClipboardKind::Cut;
|
||||
Message::Drop(Some((tab_location.clone(), data)))
|
||||
let button_row: Element<_> =
|
||||
if item.metadata.is_dir() && item.location_opt.is_some() {
|
||||
let tab_location = item.location_opt.clone().unwrap();
|
||||
let tab_location_enter = tab_location.clone();
|
||||
let tab_location_leave = tab_location.clone();
|
||||
let is_dnd_hovered =
|
||||
self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location);
|
||||
cosmic::widget::container(
|
||||
DndDestination::for_data(button_row, move |data, action| {
|
||||
if let Some(mut data) = data {
|
||||
if action == DndAction::Copy {
|
||||
Message::Drop(Some((tab_location.clone(), data)))
|
||||
} else if action == DndAction::Move {
|
||||
data.kind = ClipboardKind::Cut;
|
||||
Message::Drop(Some((tab_location.clone(), data)))
|
||||
} else {
|
||||
log::warn!("unsupported action: {:?}", action);
|
||||
Message::Drop(None)
|
||||
}
|
||||
} else {
|
||||
log::warn!("unsupported action: {:?}", action);
|
||||
log::warn!("No data for drop.");
|
||||
Message::Drop(None)
|
||||
}
|
||||
} else {
|
||||
log::warn!("No data for drop.");
|
||||
Message::Drop(None)
|
||||
}
|
||||
})
|
||||
.on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone()))
|
||||
.on_leave(move || Message::DndLeave(tab_location_leave.clone())),
|
||||
)
|
||||
// todo refactor into the dnd destination wrapper
|
||||
.style(if is_dnd_hovered {
|
||||
theme::Container::custom(|t| {
|
||||
let mut a = cosmic::iced_style::container::StyleSheet::appearance(
|
||||
t,
|
||||
&theme::Container::default(),
|
||||
);
|
||||
let t = t.cosmic();
|
||||
// todo use theme drop target color
|
||||
let mut bg = t.accent_color();
|
||||
bg.alpha = 0.2;
|
||||
a.background = Some(Color::from(bg).into());
|
||||
a.border = Border {
|
||||
color: t.accent_color().into(),
|
||||
width: 1.0,
|
||||
radius: t.radius_s().into(),
|
||||
};
|
||||
a
|
||||
})
|
||||
.on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone()))
|
||||
.on_leave(move || Message::DndLeave(tab_location_leave.clone())),
|
||||
)
|
||||
// todo refactor into the dnd destination wrapper
|
||||
.style(if is_dnd_hovered {
|
||||
theme::Container::custom(|t| {
|
||||
let mut a = cosmic::iced_style::container::StyleSheet::appearance(
|
||||
t,
|
||||
&theme::Container::default(),
|
||||
);
|
||||
let t = t.cosmic();
|
||||
// todo use theme drop target color
|
||||
let mut bg = t.accent_color();
|
||||
bg.alpha = 0.2;
|
||||
a.background = Some(Color::from(bg).into());
|
||||
a.border = Border {
|
||||
color: t.accent_color().into(),
|
||||
width: 1.0,
|
||||
radius: t.radius_s().into(),
|
||||
};
|
||||
a
|
||||
})
|
||||
} else {
|
||||
theme::Container::default()
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
theme::Container::default()
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
button_row.into()
|
||||
};
|
||||
button_row.into()
|
||||
};
|
||||
|
||||
if item.selected || !drag_items.is_empty() {
|
||||
let dnd_row = if !item.selected {
|
||||
|
|
@ -3306,8 +3365,8 @@ impl Tab {
|
|||
items
|
||||
.iter()
|
||||
.filter(|item| item.selected)
|
||||
.filter_map(|item| item.path_opt.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.filter_map(|item| item.path_opt().map(|x| x.clone()))
|
||||
.collect::<Vec<PathBuf>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let item_view = DndSource::<_, cosmic::app::Message<app::Message>, ClipboardCopy>::with_id(
|
||||
|
|
@ -3336,7 +3395,8 @@ impl Tab {
|
|||
.on_press(move |_point_opt| Message::Click(None))
|
||||
.on_release(|_| Message::ClickRelease(None))
|
||||
.on_back_press(move |_point_opt| Message::GoPrevious)
|
||||
.on_forward_press(move |_point_opt| Message::GoNext);
|
||||
.on_forward_press(move |_point_opt| Message::GoNext)
|
||||
.on_scroll(respond_to_scroll_direction);
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None));
|
||||
|
|
@ -3344,6 +3404,7 @@ impl Tab {
|
|||
mouse_area = mouse_area.on_right_press(Message::ContextMenu);
|
||||
}
|
||||
|
||||
let should_propogate_events = true;
|
||||
let mut popover = widget::popover(mouse_area);
|
||||
|
||||
if let Some(point) = self.context_menu {
|
||||
|
|
@ -3383,7 +3444,7 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
}
|
||||
Location::Networks => {
|
||||
Location::Network(uri, display_name) if uri == "network:///" => {
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children(vec![
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
|
|
@ -3485,7 +3546,7 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(path) = item.path_opt.clone() {
|
||||
if let Some(Location::Path(path)) = item.location_opt.clone() {
|
||||
subscriptions.push(subscription::channel(
|
||||
path.clone(),
|
||||
1,
|
||||
|
|
@ -3549,20 +3610,42 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: Modifiers) -> Option<Message> {
|
||||
if !modifiers.control() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let delta_y = match delta {
|
||||
ScrollDelta::Lines { y, .. } => y,
|
||||
ScrollDelta::Pixels { y, .. } => y,
|
||||
};
|
||||
|
||||
if delta_y > 0.0 {
|
||||
return Some(Message::ZoomIn);
|
||||
}
|
||||
|
||||
if delta_y < 0.0 {
|
||||
return Some(Message::ZoomOut);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use cosmic::iced_runtime::keyboard::Modifiers;
|
||||
use cosmic::{iced::mouse::ScrollDelta, iced_runtime::keyboard::Modifiers};
|
||||
use log::{debug, trace};
|
||||
use tempfile::TempDir;
|
||||
use test_log::test;
|
||||
|
||||
use super::{scan_path, Location, Message, Tab};
|
||||
use super::{respond_to_scroll_direction, scan_path, Location, Message, Tab};
|
||||
use crate::{
|
||||
app::test_utils::{
|
||||
assert_eq_tab_path, empty_fs, eq_path_item, filter_dirs, read_dir_sorted, simple_fs,
|
||||
tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED,
|
||||
assert_eq_tab_path, assert_zoom_affects_item_size, empty_fs, eq_path_item, filter_dirs,
|
||||
read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN,
|
||||
NUM_NESTED,
|
||||
},
|
||||
config::{IconSizes, TabConfig},
|
||||
};
|
||||
|
|
@ -3795,6 +3878,64 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_zoom_in_increases_item_view_size() -> io::Result<()> {
|
||||
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||
let path = fs.path();
|
||||
|
||||
let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default());
|
||||
|
||||
let should_affect_size = true;
|
||||
assert_zoom_affects_item_size(&mut tab, Message::ZoomIn, should_affect_size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tab_zoom_out_decreases_item_view_size() -> io::Result<()> {
|
||||
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||
let path = fs.path();
|
||||
|
||||
let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default());
|
||||
|
||||
let should_affect_size = true;
|
||||
assert_zoom_affects_item_size(&mut tab, Message::ZoomOut, should_affect_size);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> {
|
||||
let message_maybe =
|
||||
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::CTRL);
|
||||
assert!(!message_maybe.is_none());
|
||||
assert!(matches!(message_maybe.unwrap(), Message::ZoomIn));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {
|
||||
let message_maybe =
|
||||
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::empty());
|
||||
assert!(message_maybe.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> {
|
||||
let message_maybe =
|
||||
respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::CTRL);
|
||||
assert!(!message_maybe.is_none());
|
||||
assert!(matches!(message_maybe.unwrap(), Message::ZoomOut));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> {
|
||||
let message_maybe = respond_to_scroll_direction(
|
||||
ScrollDelta::Pixels { x: 0.0, y: -1.0 },
|
||||
Modifiers::empty(),
|
||||
);
|
||||
assert!(message_maybe.is_none());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> {
|
||||
let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?;
|
||||
|
|
@ -3942,7 +4083,7 @@ impl<M> Widget<M, cosmic::Theme, cosmic::Renderer> for ArcElementWrapper<M> {
|
|||
_clipboard: &mut dyn cosmic::iced_core::Clipboard,
|
||||
_shell: &mut cosmic::iced_core::Shell<'_, M>,
|
||||
_viewport: &Rectangle,
|
||||
) -> cosmic::iced_core::event::Status {
|
||||
) -> event::Status {
|
||||
self.0.lock().unwrap().as_widget_mut().on_event(
|
||||
_state, _event, _layout, _cursor, _renderer, _clipboard, _shell, _viewport,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue