Merge remote-tracking branch 'upstream/master' into auth-dialog-submit

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk 2025-01-27 20:31:16 +01:00
commit 16d6316118
52 changed files with 2139 additions and 1113 deletions

1105
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@ vergen = { version = "8", features = ["git", "gitcl"] }
[dependencies] [dependencies]
chrono = { version = "0.4", features = ["unstable-locales"] } chrono = { version = "0.4", features = ["unstable-locales"] }
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "5.0.1" dirs = "5.0.1"
env_logger = "0.11" env_logger = "0.11"
freedesktop_entry_parser = "1.3" freedesktop_entry_parser = "1.3"
@ -67,7 +68,7 @@ features = ["multi-window", "tokio", "winit"]
[features] [features]
default = ["bzip2", "desktop", "gvfs", "liblzma", "notify", "wgpu"] default = ["bzip2", "desktop", "gvfs", "liblzma", "notify", "wgpu"]
desktop = ["libcosmic/desktop", "dep:xdg"] desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
gvfs = ["dep:gio", "dep:glib"] gvfs = ["dep:gio", "dep:glib"]
jemalloc = ["dep:tikv-jemallocator"] jemalloc = ["dep:tikv-jemallocator"]
notify = ["dep:notify-rust"] notify = ["dep:notify-rust"]

View file

@ -3,6 +3,10 @@ name = "cosmic-files-applet"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies]
log = "0.4"
zbus = "4"
[dependencies.cosmic-files] [dependencies.cosmic-files]
path = ".." path = ".."
default-features = false default-features = false

View file

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-3.0-only
// Implementation of https://www.freedesktop.org/wiki/Specifications/file-manager-interface/
#![allow(dead_code, non_snake_case)]
use std::process;
pub struct FileManager;
impl FileManager {
//TODO: return error?
fn open(&self, uris: &[&str], _startup_id: &str) {
match process::Command::new("cosmic-files").args(uris).spawn() {
Ok(mut child) => {
log::info!("spawned cosmic-files with id {:?}", child.id());
match child.wait() {
Ok(status) => {
if status.success() {
log::info!("cosmic-files exited with {status}");
} else {
log::warn!("failed to run cosmic-files: exited with {status}");
}
}
Err(err) => {
log::warn!("failed to run cosmic-files: {err}");
}
}
}
Err(err) => {
log::warn!("failed to spawn cosmic-files: {err}");
}
}
}
}
//TODO: why does &[&str] not implement Deserialize?
#[zbus::interface(name = "org.freedesktop.FileManager1")]
impl FileManager {
fn ShowFolders(&self, URIs: Vec<&str>, StartupId: &str) {
log::warn!("ShowFolders {:?} {:?}", URIs, StartupId);
self.open(&URIs, StartupId)
}
fn ShowItems(&self, URIs: Vec<&str>, StartupId: &str) {
log::warn!("ShowItems {:?} {:?}", URIs, StartupId);
self.open(&URIs, StartupId)
}
fn ShowItemProperties(&self, URIs: Vec<&str>, StartupId: &str) {
log::warn!("ShowItemProperties {:?} {:?}", URIs, StartupId);
self.open(&URIs, StartupId)
}
}

View file

@ -1,3 +1,11 @@
mod file_manager;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
//TODO: move file manager service to its own daemon?
let _conn_res = zbus::blocking::connection::Builder::session()?
.name("org.freedesktop.FileManager1")?
.serve_at("/org/freedesktop/FileManager1", file_manager::FileManager)?
.build();
cosmic_files::desktop() cosmic_files::desktop()
} }

View file

@ -2,7 +2,7 @@ use cosmic_files::operation::{recursive::Context, Controller, ReplaceResult};
use std::{error::Error, io, path::PathBuf}; use std::{error::Error, io, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let mut context = Context::new(Controller::new()) let mut context = Context::new(Controller::default())
.on_progress(|op, progress| { .on_progress(|op, progress| {
println!("{:?}: {:?}", op.to, progress); println!("{:?}: {:?}", op.to, progress);
}) })

View file

@ -62,7 +62,7 @@ complete = انتهى
copy_noun = ينسخ copy_noun = ينسخ
## Open with ## Open with
open-with = افتح ب‍استخدام menu-open-with = افتح ب‍استخدام
default-app = {$name} (المبدئي) default-app = {$name} (المبدئي)
## Properties ## Properties

View file

@ -159,7 +159,7 @@ restored = Адноўлена {$items} {$items ->
unknown-folder = невядомая папка unknown-folder = невядомая папка
## Open with ## Open with
open-with = Адкрыць з дапамогай menu-open-with = Адкрыць з дапамогай
default-app = {$name} (па змаўчанні) default-app = {$name} (па змаўчанні)
## Properties ## Properties

View file

@ -62,7 +62,7 @@ complete = Hotovo
copy_noun = Kopírovat copy_noun = Kopírovat
## Open with ## Open with
open-with = Otevřít v menu-open-with = Otevřít v
default-app = {$name} (výchozí) default-app = {$name} (výchozí)
## Properties ## Properties

View file

@ -196,7 +196,7 @@ restored = Genoprettet {$items} {$items ->
unknown-folder = ukendt mappe unknown-folder = ukendt mappe
## Open with ## Open with
open-with = Åbn med... menu-open-with = Åbn med...
default-app = {$name} (standardindstilling) default-app = {$name} (standardindstilling)
## Show details ## Show details

View file

@ -196,7 +196,7 @@ restored = {$items} {$items ->
unknown-folder = unbekannter Ordner unknown-folder = unbekannter Ordner
## Öffnen mit ## Öffnen mit
open-with = Öffnen mit menu-open-with = Öffnen mit
default-app = {$name} (Standard) default-app = {$name} (Standard)
## Details anzeigen ## Details anzeigen

View file

@ -96,6 +96,7 @@ set-executable-and-launch-description = Do you want to set "{$name}" as executab
set-and-launch = Set and launch set-and-launch = Set and launch
## Metadata Dialog ## Metadata Dialog
open-with = Open with
owner = Owner owner = Owner
group = Group group = Group
other = Other other = Other
@ -196,7 +197,7 @@ restored = Restored {$items} {$items ->
unknown-folder = unknown folder unknown-folder = unknown folder
## Open with ## Open with
open-with = Open with... menu-open-with = Open with...
default-app = {$name} (default) default-app = {$name} (default)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = Se ha restaurado {$items} {$items ->
unknown-folder = carpeta desconocida unknown-folder = carpeta desconocida
## Open with ## Open with
open-with = Abrir con menu-open-with = Abrir con
default-app = {$name} (predeterminado) default-app = {$name} (predeterminado)
## Show details ## Show details

View file

@ -62,7 +62,7 @@ complete = Completadas
copy_noun = Copia copy_noun = Copia
## Open with ## Open with
open-with = Abrir con menu-open-with = Abrir con
default-app = {$name} (por defecto) default-app = {$name} (por defecto)
## Properties ## Properties

View file

@ -201,7 +201,7 @@ restored = Palautettu {$items} {$items ->
unknown-folder = Tuntematon kansio unknown-folder = Tuntematon kansio
## Open with ## Open with
open-with = Avaa ohjelmalla… menu-open-with = Avaa ohjelmalla…
default-app = {$name} (oletus) default-app = {$name} (oletus)
## Show details ## Show details

View file

@ -184,7 +184,7 @@ restored = {$items} {$items ->
unknown-folder = Dossier inconnu unknown-folder = Dossier inconnu
## Open with ## Open with
open-with = Ouvrir avec menu-open-with = Ouvrir avec
default-app = {$name} (défaut) default-app = {$name} (défaut)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = {$items} {$items ->
unknown-folder = अज्ञात फ़ोल्डर unknown-folder = अज्ञात फ़ोल्डर
## Open with ## Open with
open-with = इसके साथ खोलें menu-open-with = इसके साथ खोलें
default-app = {$name} (डिफ़ॉल्ट) default-app = {$name} (डिफ़ॉल्ट)
## Show details ## Show details

View file

@ -1,118 +1,220 @@
cosmic-files = COSMIC Files cosmic-files = COSMIC Fájlok
empty-folder = Üres mappa empty-folder = Üres mappa
empty-folder-hidden = Üres mappa (rejtett elemek vannak benne) empty-folder-hidden = Üres mappa (Rejtett elemek vannak benne)
no-results = Nincs találat
filesystem = Fájlrendszer filesystem = Fájlrendszer
home = Otthon home = Saját mappa
networks = Hálózatok
notification-in-progress = Fájlműveletek folyamatban vannak.
trash = Kuka trash = Kuka
recents = Legutóbbiak
undo = Visszavonás undo = Visszavonás
today = Ma
# Desktop view options
desktop-view-options = Asztali nézet beállításai...
show-on-desktop = Megjelenítés az asztalon
desktop-folder-content = Asztal mappa tartalma
mounted-drives = Csatolt meghajtók
trash-folder-icon = Kuka ikon
icon-size-and-spacing = Ikonméret és távolság
icon-size = Ikonméret
# List view # List view
name = Név name = Név
modified = Szerkesztve modified = Szerkesztve
trashed-on = Kukába helyezve
size = Méret size = Méret
# Progress footer
details = Részletek
dismiss = Üzenet elvetése
operations-running = {$running} művelet fut ({$percent}%)...
operations-running-finished = {$running} művelet fut ({$percent}%), {$finished} befejezve...
pause = Szünet
resume = Folytatás
# Dialogs # Dialogs
## Compress Dialog
create-archive = Tömörített fájl létrehozása
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = Kuka kiürítése empty-trash = Kuka ürítése
empty-trash-warning = Biztosan véglegesen törölni szeretnéd a kukában lévő összes elemet? empty-trash-warning = Biztosan véglegesen törölni szeretnéd a kukában lévő összes elemet?
## Mount Error Dialog
mount-error = A meghajtó nem elérhető
## New File/Folder Dialog ## New File/Folder Dialog
create-new-file = Új fájl létrehozása create-new-file = Új fájl létrehozása
create-new-folder = Új mappa létrehozása create-new-folder = Új mappa létrehozása
file-name = Fájlnév file-name = Fájlnév
folder-name = Mappa neve folder-name = Mappanév
file-already-exists = Már létezik ilyen nevű fájl. file-already-exists = Már létezik ilyen nevű fájl.
folder-already-exists = Már létezik ilyen nevű mappa. folder-already-exists = Már létezik ilyen nevű mappa.
name-hidden = "." karakterrel kezdődő nevek rejtve lesz. name-hidden = A ponttal kezdődő nevek rejtve lesznek.
name-invalid = A név nem lehet "{$filename}". name-invalid = A név nem lehet "{$filename}".
name-no-slashes = A név nem tartalmazhat perjelet. name-no-slashes = A név nem tartalmazhat perjelet.
## Open/Save Dialog ## Open/Save Dialog
cancel = Mégse cancel = Mégse
create = Létrehozás
open = Megnyitás open = Megnyitás
open-file = Fájl megnyitása open-file = Fájl megnyitása
open-folder = Mappa megnyitása open-folder = Mappa megnyitása
open-in-new-tab = Megnyitás új lapon open-in-new-tab = Megnyitás új lapon
open-in-new-window = Megnyitás új ablakban open-in-new-window = Megnyitás új ablakban
open-item-location = Útvonal megnyitása
open-multiple-files = Több fájl megnyitása open-multiple-files = Több fájl megnyitása
open-multiple-folders = Több mappa megnyitása open-multiple-folders = Több mappa megnyitása
save = Mentés save = Mentés
save-file = Fájl mentése save-file = Fájl mentése
## Open With Dialog
open-with-title = Hogyan szeretnéd megnyitni "{$name}"-t?
browse-store = {$store} böngészése
## Rename Dialog ## Rename Dialog
rename-file = Fájl átnevezése rename-file = Fájl átnevezése
rename-folder = Mappa átnevezése rename-folder = Mappa átnevezése
## Replace Dialog ## Replace Dialog
replace = Kicserélés replace = Csere
replace-title = A(z) {$filename} már létezik ezen a helyen. replace-title = "{$filename}" már létezik.
replace-warning = Le szeretné cserélni a mentettre? A cseréje felülírja a tartalmát. replace-warning = Le szeretnéd cserélni a mentett fájlra? A cseréje felülírja annak tartalmát.
replace-warning-operation = Ki akarod cserélni? A cseréje felülírja a tartalmát. replace-warning-operation = Ki szeretnéd cserélni? A csere felülírja annak tartalmát.
original-file = Eredeti fájl original-file = Eredeti fájl
replace-with = Kicserélés vele replace-with = Csere erre
apply-to-all = Mindenkire vonatkozzon apply-to-all = Alkalmazás mindegyikre
keep-both = Mindkettő megtartása keep-both = Mindkettő megtartása
skip = Kihagyás skip = Kihagyás
## Set as Executable and Launch Dialog
set-executable-and-launch = Futtathatóvá tétele, majd indítása
set-executable-and-launch-description = Szeretnéd futtathatóvá tenni a "{$name}" fájlt és elindítani?
set-and-launch = Alkalmazás és indítás
## Metadata Dialog
open-with = Megnyitás ezzel
owner = Tulajdonos
group = Csoport
other = Többi
read = Olvasás
write = Írás
execute = Futtatás
# Context Pages # Context Pages
## About ## About
git-description = Git commit {$hash} {$date}-kor git-description = Git commit {$hash} {$date}-kor
## Add Network Drive
add-network-drive = Hálózati meghajtó hozzáadása
connect = Csatlakozás
connect-anonymously = Csatlakozás névtelenül
connecting = Csatlakozás...
domain = Tartomány
enter-server-address = Add meg a szerver címét
network-drive-description =
A szerver címek tartalmazzák a protokoll előtagot és a címet.
Példák: ssh://192.168.0.1, ftp://[2001:db8::1]
### Make sure to keep the comma which separates the columns
network-drive-schemes =
Elérhető protokollok,Előtag
AppleTalk,afp://
File Transfer Protocol,ftp:// vagy ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// vagy ssh://
WebDav,dav:// vagy davs://
network-drive-error = A hálózati meghajtó nem elérhető
password = Jelszó
remember-password = Jelszó megjegyzése
try-again = Újra
username = Felhasználónév
## Operations ## Operations
edit-history = Előzmények szerkesztése cancelled = Megszakítva
edit-history = Szerkesztési előzmények
history = Előzmények history = Előzmények
no-history = Nincsenek tételek a előzményekben. no-history = Nem találhatók elemek az előzményekben.
pending = Függőben levő pending = Függőben
failed = Nem sikerült progress = {$percent}%
complete = Kész progress-cancelled = {$percent}%, megszakítva
progress-paused = {$percent}%, szüneteltetve
failed = Sikertelen
complete = Befejeződött
compressing = {$items} {$items ->
[one] elem
*[other] elem
} tömörítése innen: "{$from}" ide: "{$to}" ({$progress})...
compressed = {$items} {$items ->
[one] elem
*[other] elem
} tömörítve innen: "{$from}" ide: "{$to}"
copy_noun = Másolás copy_noun = Másolás
creating = {$name} létrehozása itt: {$parent} creating = "{$name}" létrehozása itt: "{$parent}"
created = {name} létrehozva itt: {$parent} created = "{$name}" létrehozva itt: "{$parent}"
copying = {$items} másolása {$items -> copying = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} {$from} és {$to} között } másolása innen: "{$from}" ide: "{$to}" ({$progress})...
copied = {$items} másolva {$items -> copied = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} {$from} és {$to} között } másolva innen: "{$from}" ide: "{$to}"
emptying-trash = {trash} ürítése emptying-trash = {trash} kiürítése ({$progress})...
emptied-trash = {trash} kiürítve emptied-trash = {trash} kiürítve
moving = {$items} mozgatása {$items -> extracting = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} from {$from} to {$to} } kicsomagolása innen: "{$from}" ide: "{$to}" ({$progress})...
moved = Moved {$items} {$items -> extracted = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} {$from} és {$to} között } kicsomagolva innen: "{$from}" ide: "{$to}"
renaming = {$from} átnevezése {$to} névre setting-executable-and-launching = Setting "{$name}" as executable and launching
renamed = {$from} átnevezve {$to} névre set-executable-and-launched = Set "{$name}" as executable and launched
restoring = {$items} visszaállítása {$items -> moving = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} {trash}-ból } áthelyezése innen: "{$from}" ide: "{$to}" ({$progress})...
restored = {$items} visszaállítva {$items -> moved = {$items} {$items ->
[one] elem [one] elem
*[other] elemek *[other] elem
} {trash}-ból } áthelyezve innen: "{$from}" ide: "{$to}"
undo = Visszavonás renaming = Átnevezés "{$from}"-ról "{$to}"-ra
renamed = Átnevezve "{$from}"-ról "{$to}"-ra
restoring = {$items} {$items ->
[one] elem
*[other] elem
} visszaállítása a {trash}ból ({$progress})...
restored = {$items} {$items ->
[one] elem
*[other] elem
} visszaállítva a {trash}ból
unknown-folder = ismeretlen mappa unknown-folder = ismeretlen mappa
## Open with ## Open with
open-with = Megnyitás ezzel menu-open-with = Megnyitás mással...
default-app = {$name} (alapértelmezett) default-app = {$name} (alapértelmezett)
## Properties ## Show details
properties = Tulajdonságok show-details = Részletek mutatása
type = Típus: {$mime}
items = Elemek: {$items}
item-size = Méret: {$size}
item-created = Létrehozva: {$created}
item-modified = Módosítva: {$modified}
item-accessed = Hozzáférve: {$accessed}
calculating = Számítás...
## Settings ## Settings
settings = Beállítások settings = Beállítások
### Appearance ### Appearance
appearance = Kinézet appearance = Megjelenés
theme = Téma theme = Téma
match-desktop = Asztallal egyező match-desktop = Asztallal egyező
dark = Sötét dark = Sötét
@ -120,23 +222,31 @@ light = Világos
# Context menu # Context menu
add-to-sidebar = Hozzáadás az oldalsávhoz add-to-sidebar = Hozzáadás az oldalsávhoz
new-file = Új fájl compress = Tömörítés
new-folder = Új mappa extract-here = Kicsomagolás itt
new-file = Új fájl...
new-folder = Új mappa...
open-in-terminal = Megnyitás a terminálban open-in-terminal = Megnyitás a terminálban
move-to-trash = A kukába mozgatás move-to-trash = Kukába helyezés
restore-from-trash = Visszaállítás a kukából restore-from-trash = Visszaállítás a kukából
remove-from-sidebar = Eltávolítás az oldalsávról remove-from-sidebar = Eltávolítás az oldalsávról
sort-by-name = Név szerinti rendezés sort-by-name = Név szerinti rendezés
sort-by-modified = Módosítás szerinti rendezés sort-by-modified = Módosítás szerinti rendezés
sort-by-size = Méret szerinti rendezés sort-by-size = Méret szerinti rendezés
sort-by-trashed = Törlés ideje szerinti rendezés
## Desktop
change-wallpaper = Háttérkép cseréje...
desktop-appearance = Asztali megjelenés...
display-settings = Képernyő beállításai...
# Menu # Menu
## File ## File
file = Fájl file = Fájl
new-tab = Új lap new-tab = Új fül
new-window = Új ablak new-window = Új ablak
rename = Átnevezés rename = Átnevezés...
close-tab = Ablak bezárása close-tab = Ablak bezárása
quit = Kilépés quit = Kilépés
@ -145,16 +255,26 @@ edit = Szerkesztés
cut = Kivágás cut = Kivágás
copy = Másolás copy = Másolás
paste = Beillesztés paste = Beillesztés
select-all = Összes kiválasztása select-all = Mind kijelölése
## View ## View
zoom-in = Benagyítás zoom-in = Nagyítás
default-size = Alapértelmezett méret default-size = Alapértelmezett méret
zoom-out = Kinagyítás zoom-out = Kicsinyítés
view = Nézet view = Nézet
grid-view = Rácsnézet grid-view = Rácsnézet
list-view = Listanézet list-view = Listanézet
show-hidden-files = Rejtett fájlok mutatása show-hidden-files = Rejtett fájlok megjelenítése
list-directories-first = Könyvtárak listázások először list-directories-first = Könyvtárak listázása először
gallery-preview = Galéria előnézet
menu-settings = Beállítások... menu-settings = Beállítások...
menu-about = A COSMIC Files-ról... menu-about = A COSMIC Fájlokról...
## Sort
sort = Rendezés
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Legújabb előre
sort-oldest-first = Legrégibb előre
sort-smallest-to-largest = Legkisebbtől a legnagyobbig
sort-largest-to-smallest = Legnagyobbtól a legkisebbig

View file

@ -192,7 +192,7 @@ restored = Ripristinato {$items} {$items ->
unknown-folder = cartella sconosciuta unknown-folder = cartella sconosciuta
## Open with ## Open with
open-with = Apri con menu-open-with = Apri con
default-app = {$name} (default) default-app = {$name} (default)
## Show details ## Show details

View file

@ -128,7 +128,7 @@ undo = 元に戻す
unknown-folder = 不明なフォルダー unknown-folder = 不明なフォルダー
## Open with ## Open with
open-with = 別のアプリケーションで開く menu-open-with = 別のアプリケーションで開く
default-app = {$name} (デフォルト) default-app = {$name} (デフォルト)
## Properties ## Properties

View file

@ -181,7 +181,7 @@ restored = {$items} {$items ->
unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್ unknown-folder = ಅಜ್ಞಾತ ಫೋಲ್ಡರ್
## Open with ## Open with
open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ menu-open-with = ಇದರೊಂದಿಗೆ ತೆರೆಯಿರಿ
default-app = {$name} (ಸ್ಥೂಲ) default-app = {$name} (ಸ್ಥೂಲ)
## Show details ## Show details

View file

@ -52,7 +52,7 @@ failed = 실패
complete = 완료 complete = 완료
## Open with ## Open with
open-with = 다른 앱으로 열기 menu-open-with = 다른 앱으로 열기
default-app = {$name} (기본) default-app = {$name} (기본)
## Properties ## Properties

275
i18n/nl/cosmic_files.ftl Normal file
View file

@ -0,0 +1,275 @@
cosmic-files = COSMIC-bestandsbeheerder
empty-folder = Lege map
empty-folder-hidden = Lege map (met verborgen bestanden)
no-results = Geen resultaten gevonden
filesystem = Bestandssysteem
home = Gebruikersmap
networks = Netwerken
notification-in-progress = Bestanden worden momenteel bewerkt.
trash = Prullenbak
recents = Recente bestanden
undo = Ongedaan maken
today = Vandaag
# Desktop view options
desktop-view-options = Opties voor bureaubladweergave
show-on-desktop = Op bureaublad weergeven
desktop-folder-content = Bestanden in Bureaublad
mounted-drives = Gekoppelde schijven
trash-folder-icon = Prullenbakicoon
icon-size-and-spacing = Grootte en ruimte tussen iconen
icon-size = Icoon grootte
# List view
name = Naam
modified = Bewerkt
trashed-on = Tijd van verwijderen
size = Grootte
# Progress footer
details = Details
dismiss = Bericht negeren
operations-running = {$running} bewerkingen worden uitgevoerd ({$percent}%)...
operations-running-finished = {$running} bewerkingen worden uitgevoerd ({$percent}%), {$finished} voltooid...
pause = Pauze
resume = Hervatten
# Dialogs
## Compress Dialog
create-archive = Maak een archiefbestand
## Empty Trash Dialog
empty-trash = Prullenbak legen?
empty-trash-warning = Weet u zeker dat u alles in de prullenbak permanent wilt verwijderen?
## Mount Error Dialog
mount-error = Toegang tot schijf niet mogelijk
## New File/Folder Dialog
create-new-file = Nieuw bestand aanmaken
create-new-folder = Nieuwe map aanmaken
file-name = Bestandsnaam
folder-name = Mapnaam
file-already-exists = Er bestaat al een bestand met deze naam.
folder-already-exists = Er bestaat al een map met deze naam.
name-hidden = Namen die met '.' beginnen worden verborgen.
name-invalid = De naam '{$filename}' is niet geldig.
name-no-slashes = De naam mag geen schuine strepen bevatten.
## Open/Save Dialog
cancel = Annuleren
create = Aanmaken
open = Openen
open-file = Bestand openen
open-folder = Map openen
open-in-new-tab = Open in nieuw tabblad
open-in-new-window = Open in nieuw venster
open-item-location = Open locatie van item
open-multiple-files = Meerdere bestanden openen
open-multiple-folders = Meerdere mappen openen
save = Opslaan
save-file = Bestand opslaan
## Open With Dialog
open-with-title = Hoe wilt u '{$name}' openen?
browse-store = Verken {$store}
## Rename Dialog
rename-file = Bestand hernoemen
rename-folder = Map hernoemen
## Replace Dialog
replace = Vervangen
replace-title = '{$filename}' bestaat al op deze locatie.
replace-warning = Wilt u het bestand vervangen door de nieuwe versie? Dit zal de bestaande inhoud overschrijven.
replace-warning-operation = Wilt u het bestand vervangen? Bestaande inhoud wordt overschreven!
original-file = Oorspronkelijk bestand
replace-with = Vervangen door
apply-to-all = Op alles toepassen
keep-both = Beide behouden
skip = Overslaan
## Set as Executable and Launch Dialog
set-executable-and-launch = Bestand uitvoerbaar maken en dan openen
set-executable-and-launch-description = Wilt u '{$name}' uitvoerbaar maken en dan openen?
set-and-launch = Maak uitvoerbaar en open
## Metadata Dialog
owner = Eigenaar
group = Groep
other = Anderen
read = Lezen
write = Schrijven
execute = Uitvoeren
# Context Pages
## About
git-description = Git commit {$hash} op {$date}
## Add Network Drive
add-network-drive = Netwerkschijf toevoegen
connect = Verbinden
connect-anonymously = Anoniem verbinden
connecting = Verbinding maken...
domain = Domein
enter-server-address = Serveradres invoeren
network-drive-description =
Serveradressen bestaan uit protocolvoorvoegsel en netwerkadres.
Voorbeelden: ssh://192.168.0.1, ftp://[2001:db8::1]
### Make sure to keep the comma which separates the columns
network-drive-schemes =
Beschikbare protocollen,Voorvoegsel
AppleTalk,afp://
File Transfer Protocol,ftp:// of ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// of ssh://
WebDav,dav:// of davs://
network-drive-error = Geen toegang tot de netwerkschijf
password = Wachtwoord
remember-password = Wachtwoord onthouden
try-again = Opnieuw proberen
username = Gebruikersnaam
## Operations
cancelled = Geannuleerd
edit-history = Geschiedenis bewerken
history = Geschiedenis
no-history = Geen items in de geschiedenis.
pending = In afwachting
progress = {$percent}%
progress-cancelled = {$percent}%, geannuleerd
progress-paused = {$percent}%, gepauzeerd
failed = Mislukt
complete = Voltooid
compressing = { $items} {$items ->
[one] bestand wordt
*[other] bestanden worden
} van '{$from}' naar '{$to}' gecomprimeerd ({$progress})...
compressed = { $items} {$items ->
[one] bestand
*[other] bestanden
} gecomprimeerd van '{$from}' naar '{$to}'
copy_noun = Kopie
creating = '{$name}' in '{$parent}' aanmaken
created = '{$name}' in '{$parent}' aangemaakt
copying = {$items} {$items ->
[one] bestand wordt
*[other] bestanden worden
} van '{$from}' naar '{$to}' gekopieerd ({$progress})...
emptying-trash = {trash} wordt geleegd ({$progress})...
emptied-trash = {trash} geleegd
extracting = {$items} {$items ->
[one] bestand wordt
*[other] bestanden worden
} van '{$from}' naar '{$to}' uitgepakt ({$progress})...
extracted = {$items} {$items ->
[one] bestand
*[other] bestanden
} uitgepakt van '{$from}' naar '{$to}'
setting-executable-and-launching = '{$name}' wordt uitvoerbaar gemaakt en geopend
set-executable-and-launched = '{$name}' uitvoerbaar maken en openen
moving = {$items} {$items ->
[one] bestand wordt
*[other] bestanden worden
} van '{$from}' naar '{$to}' verplaatst ({$progress})...
moved = {$items} {$items ->
[one] bestand
*[other] bestanden
} verplaatst van '{$form}' naar '{$to}'
renaming = '{$from}' als '{$to}' hernoemen
renamed = '{$form}' als '{$to}' hernoemd
restoring = {$items} {$items ->
[one] bestand wordt
*[other] bestanden worden
} uit {trash} teruggezet ({$progress})...
restored = {$items} {$items ->
[one] bestand
*[other] bestanden
} uit {trash} teruggezet
unknown-folder = Onbekende map
## Open with
menu-open-with = Openen met...
default-app = {$name} (standaard)
## Show details
show-details = Details weergeven
type = Type: {$mime}
items = Bestanden: {$items}
item-size = Grootte: {$size}
item-created = Aangemaakt op: {$created}
item-modified = Bewerkt op: {$modified}
item-accessed = Geopend op: {$accessed}
calculating = Wordt berekend...
## Settings
settings = Instellingen
### Appearance
appearance = Uiterlijk
theme = Thema
match-desktop = Systeemstandaard
dark = Donker
light = Licht
# Context menu
add-to-sidebar = Aan de zijbalk toevoegen
compress = Comprimeren
extract-here = Uitpakken
new-file = Nieuw bestand...
new-folder = Nieuwe map...
open-in-terminal = Openen in terminal
move-to-trash = Naar prullenbak verplaatsen
restore-from-trash = Uit prullenbak terugzetten
remove-from-sidebar = Uit de zijbalk verwijderen
sort-by-name = Sorteren op naam
sort-by-modified = Sorteren op laatst bewerkt
sort-by-size = Sorteren op grootte
sort-by-trashed = Sorteren op tijdstip van verwijderen
## Desktop
change-wallpaper = Schermachtergrond wijzigen...
desktop-appearance = Bureaublad uiterlijk...
display-settings = Beeldschermbeheer...
# Menu
## File
file = Bestand
new-tab = Nieuw tabblad
new-window = Nieuw venster
rename = Hernoemen...
close-tab = Tabblad sluiten
quit = Sluiten
## Edit
edit = Bewerken
cut = Knippen
copy = Kopiëren
paste = Plakken
select-all = Alles selecteren
## View
zoom-in = Inzoomen
default-size = Zoomniveau terugzetten
zoom-out = Uitzoomen
view = Aanzicht
grid-view = Rasterweergave
list-view = Lijstweergave
show-hidden-files = Verborgen bestanden tonen
list-directories-first = Mappen bovenaan weergeven
gallery-preview = Galerijweergave
menu-settings = Instellingen...
menu-about = Over COSMIC-bestandsbeheerder...
## Sort
sort = Sorteren
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Nieuwste bovenaan
sort-oldest-first = Oudste bovenaan
sort-smallest-to-largest = Van klein naar groot
sort-largest-to-smallest = Van groot naar klein

View file

@ -3,7 +3,7 @@ empty-folder = Pusty katalog
empty-folder-hidden = Pusty katalog (z ukrytymi plikami) empty-folder-hidden = Pusty katalog (z ukrytymi plikami)
no-results = Brak wyników no-results = Brak wyników
filesystem = System plików filesystem = System plików
home = Katalog Domowy home = Katalog domowy
networks = Sieci networks = Sieci
notification-in-progress = Operacje na plikach w toku. notification-in-progress = Operacje na plikach w toku.
trash = Kosz trash = Kosz
@ -12,7 +12,7 @@ undo = Cofnij
today = Dzisiaj today = Dzisiaj
# Desktop view options # Desktop view options
desktop-view-options = Opcje widoku pulpitu... desktop-view-options = Opcje widoku pulpitu…
show-on-desktop = Pokaż na Pulpicie show-on-desktop = Pokaż na Pulpicie
desktop-folder-content = Zawartość katalogu Pulpit desktop-folder-content = Zawartość katalogu Pulpit
mounted-drives = Podpięte dyski mounted-drives = Podpięte dyski
@ -29,8 +29,8 @@ size = Rozmiar
# Progress footer # Progress footer
details = Detale details = Detale
dismiss = Odrzuć wiadomość dismiss = Odrzuć wiadomość
operations-running = {$running} bieżące działania ({$percent}%)... operations-running = {$running} bieżące działania ({$percent}%)…
operations-running-finished = {$running} bieżące działania ({$percent}%), {$finished} ukończone... operations-running-finished = {$running} bieżące działania ({$percent}%), {$finished} ukończone…
pause = Wstrzymaj pause = Wstrzymaj
resume = Wznów resume = Wznów
@ -53,8 +53,8 @@ file-name = Nazwa pliku
folder-name = Nazwa katalogu folder-name = Nazwa katalogu
file-already-exists = Plik z taką nazwą już istnieje. file-already-exists = Plik z taką nazwą już istnieje.
folder-already-exists = Katalog z taką nazwą już istnieje. folder-already-exists = Katalog z taką nazwą już istnieje.
name-hidden = Nazwy zaczynające się na "." będą ukryte. name-hidden = Nazwy zaczynające się od „.” będą ukryte.
name-invalid = Musisz zmienić nazwę na inną z "{$filename}". name-invalid = Musisz zmienić nazwę na inną z „{$filename}”.
name-no-slashes = Nazwa nie może zawierać ukośników. name-no-slashes = Nazwa nie może zawierać ukośników.
# Open/Save Dialog # Open/Save Dialog
@ -72,7 +72,7 @@ save = Zapisz
save-file = Zapisz plik save-file = Zapisz plik
## Open With Dialog ## Open With Dialog
open-with-title = Czym chcesz otworzyć "{$name}"? open-with-title = Czym chcesz otworzyć „{$name}”?
browse-store = Przeglądaj {$store} browse-store = Przeglądaj {$store}
# Rename Dialog # Rename Dialog
@ -92,7 +92,7 @@ skip = Pomiń
## Set as Executable and Launch Dialog ## Set as Executable and Launch Dialog
set-executable-and-launch = Ustaw jako wykonywalny i uruchom set-executable-and-launch = Ustaw jako wykonywalny i uruchom
set-executable-and-launch-description = Czu chcesz ustawić "{$name}" jako wykonywalny i uruchomić? set-executable-and-launch-description = Czy chcesz ustawić plik „{$name}” jako wykonywalny i uruchomić go?
set-and-launch = Ustaw i uruchom set-and-launch = Ustaw i uruchom
## Metadata Dialog ## Metadata Dialog
@ -112,7 +112,7 @@ git-description = Git commit {$hash} z {$date}
add-network-drive = Dodaj dysk sieciowy add-network-drive = Dodaj dysk sieciowy
connect = Połącz connect = Połącz
connect-anonymously = Połącz anonimowo connect-anonymously = Połącz anonimowo
connecting = Łączenie... connecting = Łączenie…
domain = Domena domain = Domena
enter-server-address = Wprowadź adres serwera enter-server-address = Wprowadź adres serwera
network-drive-description = network-drive-description =
@ -148,50 +148,50 @@ compressing = Spakuj {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" ({$progress})... } z „{$from}” do „{$to}” ({$progress})…
compressed = Spakowano {$items} {$items -> compressed = Spakowano {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" } z „{$from}” do „{$to}”
copy_noun = Kopiuj copy_noun = Kopiuj
creating = Tworzy {$name} w {$parent} creating = Tworzy {$name} w {$parent}
created = Stworzono {$name} w {$parent} created = Stworzono {$name} w {$parent}
copying = Kopiowanie {$items} {$items -> copying = Kopiowanie {$items} {$items ->
[one] elementu [one] elementu
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" ({$progress})... } z „{$from}” do „{$to}” ({$progress})…
copied = Skopiowano {$items} {$items -> copied = Skopiowano {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" } z „{$from}” do „{$to}”
emptying-trash = Opróżnianie {trash} ({$progress})... emptying-trash = Opróżnianie {trash} ({$progress})…
emptied-trash = Opróżniono {trash} emptied-trash = Opróżniono {trash}
extracting = Wypakowywanie {$items} {$items -> extracting = Wypakowywanie {$items} {$items ->
[one] elementu [one] elementu
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" ({$progress})... } z „{$from}” do „{$to}” ({$progress})…
extracted = Wypakowano {$items} {$items -> extracted = Wypakowano {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" } z „{$from}” do „{$to}”
moving = Przenoszenie {$items} {$items -> moving = Przenoszenie {$items} {$items ->
[one] elementu [one] elementu
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" ({$progress})... } z „{$from}” do „{$to}” ({$progress})…
moved = Przeniesiono {$items} {$items -> moved = Przeniesiono {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
*[other] elementów *[other] elementów
} z "{$from}" do "{$to}" } z „{$from}” do „{$to}”
renaming = Zmieniana nazwa {$from} na {$to} renaming = Zmieniana nazwa {$from} na {$to}
renamed = Zmieniono nazwę {$from} na {$to} renamed = Zmieniono nazwę {$from} na {$to}
restoring = Przywracanie {$items} {$items -> restoring = Przywracanie {$items} {$items ->
[one] elementu [one] elementu
*[other] elementów *[other] elementów
} z {trash} ({$progress})... } z {trash} ({$progress})…
restored = Przywrócono {$items} {$items -> restored = Przywrócono {$items} {$items ->
[one] element [one] element
[few] elementy [few] elementy
@ -200,7 +200,7 @@ restored = Przywrócono {$items} {$items ->
unknown-folder = nieznany katalog unknown-folder = nieznany katalog
## Open with ## Open with
open-with = Otwórz za pomocą... menu-open-with = Otwórz za pomocą…
default-app = {$name} (domyślnie) default-app = {$name} (domyślnie)
## Show details ## Show details
@ -211,7 +211,7 @@ item-size = Rozmiar: {$size}
item-created = Utworzono: {$created} item-created = Utworzono: {$created}
item-modified = Zmodyfikowano: {$modified} item-modified = Zmodyfikowano: {$modified}
item-accessed = Otwarto: {$accessed} item-accessed = Otwarto: {$accessed}
calculating = Obliczanie... calculating = Obliczanie…
## Settings ## Settings
settings = Ustawienia settings = Ustawienia
@ -239,9 +239,9 @@ sort-by-size = Uszereguj według rozmiaru
sort-by-trashed = Uszereguj według czasu usunięcia sort-by-trashed = Uszereguj według czasu usunięcia
## Desktop ## Desktop
change-wallpaper = Zmień tapetę... change-wallpaper = Zmień tapetę…
desktop-appearance = Wygląd pulpitu... desktop-appearance = Wygląd pulpitu…
display-settings = Ustawienia wyświetlacza... display-settings = Ustawienia wyświetlacza…
# Menu # Menu
@ -249,7 +249,7 @@ display-settings = Ustawienia wyświetlacza...
file = Plik file = Plik
new-tab = Nowa karta new-tab = Nowa karta
new-window = Nowe okno new-window = Nowe okno
rename = Zmień nazwę... rename = Zmień nazwę…
close-tab = Zamknij kartę close-tab = Zamknij kartę
quit = Zamknij quit = Zamknij
@ -270,8 +270,8 @@ list-view = Widok listy
show-hidden-files = Pokaż ukryte pliki show-hidden-files = Pokaż ukryte pliki
list-directories-first = Najpierw wyświetlaj katalogi list-directories-first = Najpierw wyświetlaj katalogi
gallery-preview = Podgąd galerii gallery-preview = Podgąd galerii
menu-settings = Ustawienia... menu-settings = Ustawienia…
menu-about = O Plikach COSMIC... menu-about = O Plikach COSMIC…
## Sort ## Sort
sort = Uszereguj sort = Uszereguj

View file

@ -196,7 +196,7 @@ restored = Restaurado {$items} {$items ->
unknown-folder = pasta desconhecida unknown-folder = pasta desconhecida
## Open with ## Open with
open-with = Abrir com... menu-open-with = Abrir com...
default-app = {$name} (padrão) default-app = {$name} (padrão)
## Show details ## Show details

View file

@ -122,7 +122,7 @@ undo = Desfazer
unknown-folder = pasta desconhecida unknown-folder = pasta desconhecida
## Open with ## Open with
open-with = Abrir com... menu-open-with = Abrir com...
default-app = {$name} (predefinição) default-app = {$name} (predefinição)
## Show details ## Show details

View file

@ -181,7 +181,7 @@ restored = Restaurat {$items} {$items ->
unknown-folder = dosar necunoscut unknown-folder = dosar necunoscut
## Open with ## Open with
open-with = Deschide cu... menu-open-with = Deschide cu...
default-app = {$name} (implicit) default-app = {$name} (implicit)
## Show details ## Show details

View file

@ -161,7 +161,7 @@ restored = Восстановлено {$items} {$items ->
unknown-folder = неизвестная папка unknown-folder = неизвестная папка
## Open with ## Open with
open-with = Открыть с помощью menu-open-with = Открыть с помощью
default-app = {$name} (по умолчанию) default-app = {$name} (по умолчанию)
## Show details ## Show details

View file

@ -195,7 +195,7 @@ undo = Späť
unknown-folder = neznámy priečinok unknown-folder = neznámy priečinok
## Open with ## Open with
open-with = Otvoriť s menu-open-with = Otvoriť s
default-app = {$name} (Predvolené) default-app = {$name} (Predvolené)
## Show details ## Show details

View file

@ -1,90 +1,130 @@
cosmic-files = COSMIC Files
empty-folder = Tom katalog empty-folder = Tom katalog
empty-folder-hidden = Tom katalog (har dolda objekt) empty-folder-hidden = Tom katalog (har dolda objekt)
no-results = Inga resultat hittades
filesystem = Filsystem filesystem = Filsystem
home = Hem home = Hem
networks = Nätverk networks = Nätverk
notification-in-progress = Filoperationer pågår.
trash = Papperskorg trash = Papperskorg
recents = Senaste recents = Senaste
undo = Ångra undo = Ångra
today = Idag today = Idag
# Dialog # Skrivbordsvyalternativ
desktop-view-options = Skrivbordsvyalternativ...
show-on-desktop = Visa på skrivbord
desktop-folder-content = Skrivbordsmappinnehåll
mounted-drives = Monterade enheter
trash-folder-icon = Ikon för papperskorgen
icon-size-and-spacing = Ikonstorlek och mellanrum
icon-size = Ikonstorlek
# Dialogruta
cancel = Avbryt cancel = Avbryt
open = Öppna open = Öppna
# List view # Dialogrutor
## Komprimera dialogruta
create-archive = Skapa arkiv
## Töm papperskorgen dialogruta
empty-trash = Töm papperskorgen
empty-trash-warning = Är du säker på att du vill ta bort alla objekt i papperskorgen permanent?
## Monteringsfel dialogruta
mount-error = Kan inte komma åt enheten
## Ny Fil/katalog dialogruta
create-new-file = Skapa ny fil
create-new-folder = Skapa ny katalog
file-name = Filnamn
folder-name = Katalognamn
file-already-exists = En fil med det namnet finns redan.
folder-already-exists = En katalog med det namnet finns redan.
name-hidden = Namn som börjar med "." kommer att vara dolda.
name-invalid = Namnet kan inte vara "{$filename}".
name-no-slashes = Namnet får inte innehålla snedstreck.
## Öppna/Spara dialogruta
cancel = Avbryt
create = Skapa
open = Öppna
open-file = Öppna fil
open-folder = Öppna katalog
open-in-new-tab = Öppna i en ny flik
open-in-new-window = Öppna i nytt fönster
open-item-location = Öppna objektets plats
open-multiple-files = Öppna flera filer
open-multiple-folders = Öppna flera kataloger
save = Spara
save-file = Spara fil
## Öppna med dialogruta
open-with-title = Hur vill du öppna "{$name}"?
browse-store = Bläddra i {$store}
## Byt namn dialogruta
rename-file = Byt namn på fil
rename-folder = Byt namn på katalog
## Ersätt dialogruta
replace = Ersätt
replace-title = "{$filename}" existerar redan på den här platsen.
replace-warning = Vill du ersätta den med den du sparar? Om du ersätter den kommer dess innehåll att skrivas över.
replace-warning-operation = Vill du ersätta den? Om du ersätter den kommer dess innehåll att skrivas över.
original-file = Originalfil
replace-with = Ersätt med
apply-to-all = Verkställ för alla
keep-both = Behåll båda
skip = Hoppa över
## Ställ in som körbar och starta dialogruta
set-executable-and-launch = Ställ in som körbar och starta
set-executable-and-launch-description = Vill du ställa in "{$name}" som körbar och starta den?
set-and-launch = Ställ in och starta
## Metadata dialogruta
open-with = Öppna med
owner = Ägare
group = Grupp
other = Andra
read = Läs
write = Skriv
execute = Exekvera
# Listvy
name = Namn name = Namn
modified = Modifierad modified = Modifierad
trashed-on = Kastad
size = Storlek size = Storlek
# Context Pages # Framstegssidfot
details = Detaljer
dismiss = Stäng meddelande
operations-running = {$running} operationer körs ({$percent}%)...
operations-running-finished = {$running} operationer körs ({$percent}%), {$finished} färdig...
pause = Paus
resume = Återuppta
## Operations # Kontextsidor
operations = Operationer
pending = Väntar
failed = Misslyckades
complete = Slutförd
history = Historik
## Properties
properties = Egenskaper
## Settings
settings = Inställningar
### Appearance
appearance = Utseende
theme = Tema
match-desktop = Matcha skrivbordet
dark = Mörkt
light = Ljust
# Context menu
new-file = Ny fil
new-folder = Ny katalog
add-to-sidebar = Lägg till i sidofält
move-to-trash = Flytta till papperskorg
restore-from-trash = Återställ från papperskorgen
# Menu
## File
file = Fil
new-tab = Ny flik
new-window = Nytt fönster
rename = Byt namn...
menu-show-details = Visa detaljer...
close-tab = Stäng flik
quit = Avsluta
## Edit
edit = Redigera
cut = Klipp ut
copy = Kopiera
paste = Klistra in
select-all = Välj alla
## View
zoom-in = Zooma in
default-size = Standardstorlek
zoom-out = Zooma ut
view = Visa
grid-view = Rutnätsvy
list-view = Listvy
show-hidden-files = Visa dolda filer
list-directories-first = Lista kataloger först
gallery-preview = Galleri förhandsvisning
menu-settings = Inställningar...
menu-about = Om COSMIC Files...
## Open with
open-with = Öppna med...
default-app = {$name} (standard)
## Show details
show-details = Visa detaljer
## Om
git-description = Git commit {$hash} på {$date}
## 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
network-drive-description = network-drive-description =
Serveradresser inkluderar ett protokollprefix och en adress. Serveradresser inkluderar ett protokollprefix och en adress.
Exempel: ssh://192.168.0.1, ftp://[2001:db8::1] Exempel: ssh://192.168.0.1, ftp://[2001:db8::1]
@ -101,17 +141,148 @@ network-drive-error = Kan inte komma åt nätverksenheten
password = Lösenord password = Lösenord
remember-password = Kom ihåg lösenord remember-password = Kom ihåg lösenord
## Lägg till en Nätverksenhet ## Operationer
add-network-drive = Lägg till en Nätverksenhet cancelled = Avbruten
connect = Anslut edit-history = Redigera historik
connect-anonymously = Anslut anonymt history = Historik
connecting = Ansluter... no-history = Inga objekt i historiken.
domain = Domän pending = Väntar
enter-server-address = Ange server address progress = {$percent}%
try-again = Försök igen progress-cancelled = {$percent}%, avbruten
username = Användarnamn progress-paused = {$percent}%, pausad
failed = Misslyckades
complete = Färdig
compressing = Komprimerar {$items} {$items ->
[one] item
*[other] items
} from "{$from}" to "{$to}" ({$progress})...
compressed = Komprimerade {$items} {$items ->
[one] item
*[other] items
} from "{$from}" to "{$to}"
copy_noun = Koperia
creating = Skapar "{$name}" i "{$parent}"
created = Skapade "{$name}" i "{$parent}"
copying = Kopierar {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}" ({$progress})...
copied = Kopierade {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}"
emptying-trash = Tömmer {trash} ({$progress})...
emptied-trash = Tömde {trash}
extracting = Packar upp {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}" ({$progress})...
extracted = Packade upp {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}"
setting-executable-and-launching = Ställer in "{$name}" som exekverbar och startar
set-executable-and-launched = Ställ in "{$name}" som exekverbar och startar
moving = Flyttar {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}" ({$progress})...
moved = Flyttade {$items} {$items ->
[one] objekt
*[other] flera objekt
} från "{$from}" till "{$to}"
renaming = Byter namn "{$from}" till "{$to}"
renamed = Bytt namn "{$from}" till "{$to}"
restoring = Återställer {$items} {$items ->
[one] objekt
*[other] flera objekt
} från {trash} ({$progress})...
restored = Återställt {$items} {$items ->
[one] objekt
*[other] flera objekt
} från {trash}
unknown-folder = okänd katalog
## Sort ## Öppna med
menu-open-with = Öppna med...
default-app = {$name} (default)
## Visa detaljer
show-details = Visa detaljer
type = Typ: {$mime}
items = Objekt: {$items}
item-size = Storlek: {$size}
item-created = Skapad: {$created}
item-modified = Modifierad: {$modified}
item-accessed = Åtkomst: {$accessed}
calculating = Beräknar...
## Egenskaper
properties = Egenskaper
## Inställningar
settings = Inställningar
### Utseende
appearance = Utseende
theme = Tema
match-desktop = Matcha skrivbordet
dark = Mörkt
light = Ljust
# Kontext meny
add-to-sidebar = Lägg till i sidofält
compress = Komprimera
extract-here = Packa upp
new-file = Ny fil
new-folder = Ny katalog
open-in-terminal = Öppna i terminal
move-to-trash = Flytta till papperskorg
restore-from-trash = Återställ från papperskorgen
remove-from-sidebar = Ta bort från sidofält
sort-by-name = Sortera efter namn
sort-by-modified = Sortera efter modifierad
sort-by-size = Sortera efter storlek
sort-by-trashed = Sortera efter borttagningstid
## Skrivbord
change-wallpaper = Byt bakgrund...
desktop-appearance = Skrivbordsutseende...
display-settings = Skärminställningar...
# Meny
## Fil
file = Fil
new-tab = Ny flik
new-window = Nytt fönster
rename = Byt namn...
menu-show-details = Visa detaljer...
close-tab = Stäng flik
quit = Avsluta
## Redigera
edit = Redigera
cut = Klipp ut
copy = Kopiera
paste = Klistra in
select-all = Välj alla
## Visa
zoom-in = Zooma in
default-size = Standardstorlek
zoom-out = Zooma ut
view = Visa
grid-view = Rutnätsvy
list-view = Listvy
show-hidden-files = Visa dolda filer
list-directories-first = Lista kataloger först
gallery-preview = Galleri förhandsvisning
menu-settings = Inställningar...
menu-about = Om COSMIC Files...
## Sortera
sort = Sortera sort = Sortera
sort-a-z = A-Z sort-a-z = A-Z
sort-z-a = Z-A sort-z-a = Z-A
@ -119,3 +290,7 @@ sort-newest-first = Nyaste först
sort-oldest-first = Äldst först sort-oldest-first = Äldst först
sort-smallest-to-largest = Minsta till största sort-smallest-to-largest = Minsta till största
sort-largest-to-smallest = Största till minsta sort-largest-to-smallest = Största till minsta
## Visa detaljer
show-details = Visa detaljer

View file

@ -196,7 +196,7 @@ restored = Restored {$items} {$items ->
unknown-folder = แฟ้มที่ไม่รู้จัก unknown-folder = แฟ้มที่ไม่รู้จัก
## Open with ## Open with
open-with = เปิดด้วย... menu-open-with = เปิดด้วย...
default-app = {$name} (ค่าเริ่มต้น) default-app = {$name} (ค่าเริ่มต้น)
## Show details ## Show details

View file

@ -166,7 +166,7 @@ restored = {$items} öge "{$trash}"ten "{$to}" dizinine geri yüklenildi ({$prog
unknown-folder = bilinmeyen klasör unknown-folder = bilinmeyen klasör
## Open with ## Open with
open-with = Birlikte aç... menu-open-with = Birlikte aç...
default-app = {$name} (varsayılan) default-app = {$name} (varsayılan)
## Show details ## Show details

View file

@ -106,7 +106,7 @@ undo = Скасувати
unknown-folder = невідома тека unknown-folder = невідома тека
## Open with ## Open with
open-with = Відкрити за допомогою menu-open-with = Відкрити за допомогою
default-app = {$name} (типово) default-app = {$name} (типово)
## Properties ## Properties

View file

@ -166,7 +166,7 @@ restored = 已从 {trash} 还原 {$items} 个项目
unknown-folder = 未知文件夹 unknown-folder = 未知文件夹
## Open with ## Open with
open-with = 打开方式... menu-open-with = 打开方式...
default-app = {$name} (默认) default-app = {$name} (默认)
## Show details ## Show details

View file

@ -159,7 +159,7 @@ restored = 已還原 {$items} 項目 {$items ->
unknown-folder = 未知資料夾 unknown-folder = 未知資料夾
## Open with ## Open with
open-with = 開啟方式... menu-open-with = 開啟方式...
default-app = {$name} (預設) default-app = {$name} (預設)
## Show details ## Show details

View file

@ -3,7 +3,8 @@
Name=COSMIC Files Name=COSMIC Files
Name[pl]=Pliki COSMIC Name[pl]=Pliki COSMIC
Name[pt-BR]=Arquivos do COSMIC Name[pt-BR]=Arquivos do COSMIC
Exec=cosmic-files %F Name[hu]=COSMIC Fájlok
Exec=cosmic-files %U
Terminal=false Terminal=false
Type=Application Type=Application
StartupNotify=true StartupNotify=true
@ -12,4 +13,5 @@ Categories=COSMIC;Utility;FileManager;
Keywords=Folder;Manager; Keywords=Folder;Manager;
Keywords[pl]=Katalogi;Pliki;Menedżer; Keywords[pl]=Katalogi;Pliki;Menedżer;
Keywords[pt-BR]=Pastas;Gerenciador; Keywords[pt-BR]=Pastas;Gerenciador;
Keywords[hu]=mappa;fájl;kezelő
MimeType=inode/directory; MimeType=inode/directory;

View file

@ -49,7 +49,7 @@ use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
env, fmt, fs, io, env, fmt, fs, io,
num::NonZeroU16, num::NonZeroU16,
path::PathBuf, path::{Path, PathBuf},
process, process,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::{self, Instant}, time::{self, Instant},
@ -125,6 +125,8 @@ pub enum Action {
Rename, Rename,
RestoreFromTrash, RestoreFromTrash,
SearchActivate, SearchActivate,
SelectFirst,
SelectLast,
SelectAll, SelectAll,
SetSort(HeadingOptions, bool), SetSort(HeadingOptions, bool),
Settings, Settings,
@ -190,6 +192,8 @@ impl Action {
Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt),
Action::SearchActivate => Message::SearchActivate, Action::SearchActivate => Message::SearchActivate,
Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll), Action::SelectAll => Message::TabMessage(entity_opt, tab::Message::SelectAll),
Action::SelectFirst => Message::TabMessage(entity_opt, tab::Message::SelectFirst),
Action::SelectLast => Message::TabMessage(entity_opt, tab::Message::SelectLast),
Action::SetSort(sort, dir) => { Action::SetSort(sort, dir) => {
Message::TabMessage(entity_opt, tab::Message::SetSort(*sort, *dir)) Message::TabMessage(entity_opt, tab::Message::SetSort(*sort, *dir))
} }
@ -352,6 +356,7 @@ pub enum Message {
WindowClose, WindowClose,
WindowCloseRequested(window::Id), WindowCloseRequested(window::Id),
WindowNew, WindowNew,
WindowUnfocus,
ZoomDefault(Option<Entity>), ZoomDefault(Option<Entity>),
ZoomIn(Option<Entity>), ZoomIn(Option<Entity>),
ZoomOut(Option<Entity>), ZoomOut(Option<Entity>),
@ -439,7 +444,6 @@ pub enum DialogPage {
OpenWith { OpenWith {
path: PathBuf, path: PathBuf,
mime: mime_guess::Mime, mime: mime_guess::Mime,
apps: Vec<mime_app::MimeApp>,
selected: usize, selected: usize,
store_opt: Option<mime_app::MimeApp>, store_opt: Option<mime_app::MimeApp>,
}, },
@ -509,6 +513,7 @@ pub struct App {
dialog_text_input: widget::Id, dialog_text_input: widget::Id,
key_binds: HashMap<KeyBind, Action>, key_binds: HashMap<KeyBind, Action>,
margin: HashMap<window::Id, (f32, f32, f32, f32)>, margin: HashMap<window::Id, (f32, f32, f32, f32)>,
mime_app_cache: mime_app::MimeAppCache,
modifiers: Modifiers, modifiers: Modifiers,
mounter_items: HashMap<MounterKey, MounterItems>, mounter_items: HashMap<MounterKey, MounterItems>,
network_drive_connecting: Option<(MounterKey, String)>, network_drive_connecting: Option<(MounterKey, String)>,
@ -587,7 +592,7 @@ impl App {
} }
// Try mime apps, which should be faster than xdg-open // Try mime apps, which should be faster than xdg-open
for app in mime_app::mime_apps(&mime) { for app in self.mime_app_cache.get(&mime) {
let Some(mut command) = app.command(Some(path.clone().into())) else { let Some(mut command) = app.command(Some(path.clone().into())) else {
continue; continue;
}; };
@ -673,7 +678,6 @@ impl App {
let mut overlaps: HashMap<_, _> = self let mut overlaps: HashMap<_, _> = self
.windows .windows
.keys() .keys()
.into_iter()
.map(|k| (*k, (0., 0., 0., 0.))) .map(|k| (*k, (0., 0., 0., 0.)))
.collect(); .collect();
let mut sorted_overlaps: Vec<_> = self.overlap.values().collect(); let mut sorted_overlaps: Vec<_> = self.overlap.values().collect();
@ -685,7 +689,7 @@ impl App {
let tr = tr.intersects(overlap); let tr = tr.intersects(overlap);
let bl = bl.intersects(overlap); let bl = bl.intersects(overlap);
let br = br.intersects(overlap); let br = br.intersects(overlap);
let Some((top, left, bottom, right)) = overlaps.get_mut(&w_id) else { let Some((top, left, bottom, right)) = overlaps.get_mut(w_id) else {
continue; continue;
}; };
if tl && tr { if tl && tr {
@ -772,7 +776,7 @@ impl App {
Task::batch([ Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(entity, location, selection_paths), self.update_tab(entity, location, selection_paths),
]), ]),
) )
} }
@ -793,17 +797,14 @@ impl App {
self.progress_operations.insert(id); self.progress_operations.insert(id);
} }
self.pending_operations self.pending_operations
.insert(id, (operation, Controller::new())); .insert(id, (operation, Controller::default()));
} }
fn remove_window(&mut self, id: &window::Id) { fn remove_window(&mut self, id: &window::Id) {
match self.windows.remove(id) { if let Some(WindowKind::Desktop(entity)) = self.windows.remove(id) {
Some(WindowKind::Desktop(entity)) => {
// Remove the tab from the tab model // Remove the tab from the tab model
self.tab_model.remove(entity); self.tab_model.remove(entity);
} }
_ => {}
}
} }
fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task<Message> { fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task<Message> {
@ -812,7 +813,7 @@ impl App {
let Some(tab) = self.tab_model.data::<Tab>(entity) else { let Some(tab) = self.tab_model.data::<Tab>(entity) else {
return Task::none(); return Task::none();
}; };
let Some(ref items) = tab.items_opt() else { let Some(items) = tab.items_opt() else {
return Task::none(); return Task::none();
}; };
for item in items.iter() { for item in items.iter() {
@ -828,7 +829,20 @@ impl App {
return Task::none(); return Task::none();
} }
} }
self.rescan_tab(entity, tab.location.clone(), Some(op_sel.selected)) self.update_tab(entity, tab.location.clone(), Some(op_sel.selected))
}
fn update_tab(
&mut self,
entity: Entity,
location: Location,
selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> {
if let Location::Search(_, term, ..) = location {
self.search_set(entity, Some(term), selection_paths)
} else {
self.rescan_tab(entity, location, selection_paths)
}
} }
fn rescan_tab( fn rescan_tab(
@ -872,19 +886,11 @@ impl App {
let mut commands = Vec::with_capacity(needs_reload.len()); let mut commands = Vec::with_capacity(needs_reload.len());
for (entity, location) in needs_reload { for (entity, location) in needs_reload {
commands.push(self.rescan_tab(entity, location, None)); commands.push(self.update_tab(entity, location, None));
} }
Task::batch(commands) Task::batch(commands)
} }
fn search(&mut self) -> Task<Message> {
if let Some(term) = self.search_get() {
self.search_set_active(Some(term.to_string()))
} else {
Task::none()
}
}
fn search_get(&self) -> Option<&str> { fn search_get(&self) -> Option<&str> {
let entity = self.tab_model.active(); let entity = self.tab_model.active();
let tab = self.tab_model.data::<Tab>(entity)?; let tab = self.tab_model.data::<Tab>(entity)?;
@ -896,10 +902,15 @@ impl App {
fn search_set_active(&mut self, term_opt: Option<String>) -> Task<Message> { fn search_set_active(&mut self, term_opt: Option<String>) -> Task<Message> {
let entity = self.tab_model.active(); let entity = self.tab_model.active();
self.search_set(entity, term_opt) self.search_set(entity, term_opt, None)
} }
fn search_set(&mut self,tab: Entity, term_opt: Option<String>) -> Task<Message> { fn search_set(
&mut self,
tab: Entity,
term_opt: Option<String>,
selection_paths: Option<Vec<PathBuf>>,
) -> Task<Message> {
let mut title_location_opt = None; let mut title_location_opt = None;
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) { if let Some(tab) = self.tab_model.data_mut::<Tab>(tab) {
let location_opt = match term_opt { let location_opt = match term_opt {
@ -930,7 +941,7 @@ impl App {
return Task::batch([ return Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(tab, location, None), self.rescan_tab(tab, location, selection_paths),
if focus_search { if focus_search {
widget::text_input::focus(self.search_id.clone()) widget::text_input::focus(self.search_id.clone())
} else { } else {
@ -989,7 +1000,7 @@ impl App {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
tab.location = location.clone(); tab.location = location.clone();
} }
commands.push(self.rescan_tab(entity, location, None)); commands.push(self.update_tab(entity, location, None));
} }
Task::batch(commands) Task::batch(commands)
} }
@ -1416,21 +1427,24 @@ impl App {
entity_opt: &Option<Entity>, entity_opt: &Option<Entity>,
kind: &'a PreviewKind, kind: &'a PreviewKind,
context_drawer: bool, context_drawer: bool,
) -> Element<'a, Message> { ) -> Element<'a, tab::Message> {
let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_l, .. } = theme::active().cosmic().spacing;
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { PreviewKind::Custom(PreviewItem(item)) => {
children.push(item.preview_view(IconSizes::default())); children.push(item.preview_view(Some(&self.mime_app_cache), IconSizes::default()));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { if item.location_opt.as_ref() == Some(location) {
children.push(item.preview_view(tab.config.icon_sizes)); children.push(item.preview_view(
Some(&self.mime_app_cache),
tab.config.icon_sizes,
));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1444,7 +1458,10 @@ impl App {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.preview_view(tab.config.icon_sizes)); children.push(item.preview_view(
Some(&self.mime_app_cache),
tab.config.icon_sizes,
));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1452,7 +1469,10 @@ impl App {
} }
if children.is_empty() { if children.is_empty() {
if let Some(item) = &tab.parent_item_opt { if let Some(item) = &tab.parent_item_opt {
children.push(item.preview_view(tab.config.icon_sizes)); children.push(item.preview_view(
Some(&self.mime_app_cache),
tab.config.icon_sizes,
));
} }
} }
} }
@ -1558,6 +1578,7 @@ impl Application for App {
dialog_text_input: widget::Id::unique(), dialog_text_input: widget::Id::unique(),
key_binds, key_binds,
margin: HashMap::new(), margin: HashMap::new(),
mime_app_cache: mime_app::MimeAppCache::new(),
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
mounter_items: HashMap::new(), mounter_items: HashMap::new(),
network_drive_connecting: None, network_drive_connecting: None,
@ -1589,6 +1610,18 @@ impl Application for App {
let mut commands = vec![app.update_config()]; let mut commands = vec![app.update_config()];
for location in flags.locations { for location in flags.locations {
if let Some(path) = location.path_opt() {
if path.is_file() {
if let Some(parent) = path.parent() {
commands.push(app.open_tab(
Location::Path(parent.to_path_buf()),
true,
Some(vec![path.to_path_buf()]),
));
continue;
}
}
}
commands.push(app.open_tab(location, true, None)); commands.push(app.open_tab(location, true, None));
} }
@ -1661,7 +1694,7 @@ impl Application for App {
NavMenuAction::Open(entity), NavMenuAction::Open(entity),
)); ));
items.push(cosmic::widget::menu::Item::Button( items.push(cosmic::widget::menu::Item::Button(
fl!("open-with"), fl!("menu-open-with"),
None, None,
NavMenuAction::OpenWith(entity), NavMenuAction::OpenWith(entity),
)); ));
@ -1716,7 +1749,7 @@ impl Application for App {
return self.update(message); return self.update(message);
} }
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() { if let Some(data) = self.nav_model.data::<MounterData>(entity) {
if let Some(mounter) = MOUNTERS.get(&data.0) { if let Some(mounter) = MOUNTERS.get(&data.0) {
return mounter.mount(data.1.clone()).map(|_| message::none()); return mounter.mount(data.1.clone()).map(|_| message::none());
} }
@ -1733,15 +1766,12 @@ impl Application for App {
} }
fn on_context_drawer(&mut self) -> Task<Self::Message> { fn on_context_drawer(&mut self) -> Task<Self::Message> {
match self.context_page { if let ContextPage::Preview(..) = self.context_page {
ContextPage::Preview(..) => {
// Persist state of preview page // Persist state of preview page
if self.core.window.show_context != self.config.show_details { if self.core.window.show_context != self.config.show_details {
return self.update(Message::Preview(None)); return self.update(Message::Preview(None));
} }
} }
_ => {}
}
Task::none() Task::none()
} }
@ -1900,12 +1930,14 @@ impl Application for App {
} }
} }
Message::DesktopViewOptions => { Message::DesktopViewOptions => {
let mut settings = window::Settings::default(); let mut settings = window::Settings {
settings.decorations = true; decorations: true,
settings.min_size = Some(Size::new(360.0, 180.0)); min_size: Some(Size::new(360.0, 180.0)),
settings.resizable = true; resizable: true,
settings.size = Size::new(480.0, 444.0); size: Size::new(480.0, 444.0),
settings.transparent = true; transparent: true,
..Default::default()
};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -1989,11 +2021,11 @@ impl Application for App {
} }
DialogPage::OpenWith { DialogPage::OpenWith {
path, path,
apps, mime,
selected, selected,
.. ..
} => { } => {
if let Some(app) = apps.get(selected) { if let Some(app) = self.mime_app_cache.get(&mime).get(selected) {
if let Some(mut command) = app.command(Some(path.clone().into())) { if let Some(mut command) = app.command(Some(path.clone().into())) {
match spawn_detached(&mut command) { match spawn_detached(&mut command) {
Ok(()) => { Ok(()) => {
@ -2053,16 +2085,17 @@ impl Application for App {
} }
Message::ExtractHere(entity_opt) => { Message::ExtractHere(entity_opt) => {
let paths = self.selected_paths(entity_opt); let paths = self.selected_paths(entity_opt);
if let Some(current_path) = paths.get(0) { if let Some(destination) = paths
if let Some(destination) = current_path.parent().zip(current_path.file_stem()) { .first()
let destination_path = destination.0.to_path_buf(); .and_then(|first| first.parent())
.map(|parent| parent.to_path_buf())
{
self.operation(Operation::Extract { self.operation(Operation::Extract {
paths, paths,
to: destination_path, to: destination,
}); });
} }
} }
}
Message::Key(modifiers, key) => { Message::Key(modifiers, key) => {
let entity = self.tab_model.active(); let entity = self.tab_model.active();
for (key_bind, action) in self.key_binds.iter() { for (key_bind, action) in self.key_binds.iter() {
@ -2102,14 +2135,12 @@ impl Application for App {
let mut still_mounted = false; let mut still_mounted = false;
for item in mounter_items.iter() { for item in mounter_items.iter() {
if let Some(path) = item.path() { if let Some(path) = item.path() {
if path == old_path { if path == old_path && item.is_mounted() {
if item.is_mounted() {
still_mounted = true; still_mounted = true;
break; break;
} }
} }
} }
}
if !still_mounted { if !still_mounted {
unmounted.push(Location::Path(old_path)); unmounted.push(Location::Path(old_path));
} }
@ -2137,7 +2168,7 @@ impl Application for App {
}; };
if let Some(title) = title_opt { if let Some(title) = title_opt {
self.tab_model.text_set(entity, title); self.tab_model.text_set(entity, title);
commands.push(self.rescan_tab(entity, home_location.clone(), None)); commands.push(self.update_tab(entity, home_location.clone(), None));
} }
} }
if !commands.is_empty() { if !commands.is_empty() {
@ -2181,6 +2212,7 @@ impl Application for App {
auth, auth,
auth_tx, auth_tx,
}); });
return widget::text_input::focus(self.dialog_text_input.clone());
} }
Message::NetworkDriveInput(input) => { Message::NetworkDriveInput(input) => {
self.network_drive_input = input; self.network_drive_input = input;
@ -2251,7 +2283,7 @@ impl Application for App {
let mut contains_change = false; let mut contains_change = false;
for event in events.iter() { for event in events.iter() {
for event_path in event.paths.iter() { for event_path in event.paths.iter() {
if event_path.starts_with(&path) { if event_path.starts_with(path) {
match event.kind { match event.kind {
notify::EventKind::Modify( notify::EventKind::Modify(
notify::event::ModifyKind::Metadata(_), notify::event::ModifyKind::Metadata(_),
@ -2265,16 +2297,17 @@ impl Application for App {
for item in items.iter_mut() { for item in items.iter_mut() {
if item.path_opt() == Some(event_path) { if item.path_opt() == Some(event_path) {
//TODO: reload more, like mime types? //TODO: reload more, like mime types?
match fs::metadata(&event_path) { match fs::metadata(event_path) {
Ok(new_metadata) => match &mut item Ok(new_metadata) => {
.metadata if let ItemMetadata::Path {
{
ItemMetadata::Path {
metadata, metadata,
.. ..
} => *metadata = new_metadata, } = &mut item.metadata
_ => {} {
}, *metadata = new_metadata
}
}
Err(err) => { Err(err) => {
log::warn!("failed to reload metadata for {:?}: {}", path, err); log::warn!("failed to reload metadata for {:?}: {}", path, err);
} }
@ -2302,11 +2335,7 @@ impl Application for App {
let mut commands = Vec::with_capacity(needs_reload.len()); let mut commands = Vec::with_capacity(needs_reload.len());
for (entity, location) in needs_reload { for (entity, location) in needs_reload {
if let Location::Search(_, term, ..) = location { commands.push(self.update_tab(entity, location, None));
commands.push(self.search_set(entity, Some(term)));
} else {
commands.push(self.rescan_tab(entity, location, None));
}
} }
return Task::batch(commands); return Task::batch(commands);
} }
@ -2321,7 +2350,7 @@ impl Application for App {
} }
}, },
Message::OpenTerminal(entity_opt) => { Message::OpenTerminal(entity_opt) => {
if let Some(terminal) = mime_app::terminal() { if let Some(terminal) = self.mime_app_cache.terminal() {
let mut paths = Vec::new(); let mut paths = Vec::new();
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) {
@ -2389,34 +2418,24 @@ impl Application for App {
Message::OpenItemLocation(entity_opt) => { Message::OpenItemLocation(entity_opt) => {
return Task::batch(self.selected_paths(entity_opt).into_iter().filter_map( return Task::batch(self.selected_paths(entity_opt).into_iter().filter_map(
|path| { |path| {
if let Some(parent) = path.parent() { path.parent().map(Path::to_path_buf).map(|parent| {
Some(self.open_tab( self.open_tab(Location::Path(parent), true, Some(vec![path]))
Location::Path(parent.to_path_buf()), })
true,
Some(vec![path]),
))
} else {
None
}
}, },
)) ))
} }
Message::OpenWithBrowse => match self.dialog_pages.pop_front() { Message::OpenWithBrowse => match self.dialog_pages.pop_front() {
Some(DialogPage::OpenWith { Some(DialogPage::OpenWith {
mime, store_opt, .. mime,
store_opt: Some(app),
..
}) => { }) => {
if let Some(app) = store_opt {
let url = format!("mime:///{mime}"); let url = format!("mime:///{mime}");
if let Some(mut command) = app.command(Some(url.clone().into())) { if let Some(mut command) = app.command(Some(url.clone().into())) {
match spawn_detached(&mut command) { match spawn_detached(&mut command) {
Ok(()) => {} Ok(()) => {}
Err(err) => { Err(err) => {
log::warn!( log::warn!("failed to open {:?} with {:?}: {}", url, app.id, err)
"failed to open {:?} with {:?}: {}",
url,
app.id,
err
)
} }
} }
} else { } else {
@ -2427,7 +2446,6 @@ impl Application for App {
); );
} }
} }
}
Some(dialog_page) => { Some(dialog_page) => {
self.dialog_pages.push_front(dialog_page); self.dialog_pages.push_front(dialog_page);
} }
@ -2447,12 +2465,13 @@ impl Application for App {
return self.update(Message::DialogPush(DialogPage::OpenWith { return self.update(Message::DialogPush(DialogPage::OpenWith {
path: path.to_path_buf(), path: path.to_path_buf(),
mime: item.mime.clone(), mime: item.mime.clone(),
apps: item.open_with.clone(),
selected: 0, selected: 0,
store_opt: "x-scheme-handler/mime" store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>() .parse::<mime_guess::Mime>()
.ok() .ok()
.and_then(|mime| mime_app::mime_apps(&mime).first().cloned()), .and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned()
}),
})); }));
} }
} }
@ -2507,7 +2526,7 @@ impl Application for App {
Message::PendingCancelAll => { Message::PendingCancelAll => {
for (id, (_, controller)) in self.pending_operations.iter() { for (id, (_, controller)) in self.pending_operations.iter() {
controller.cancel(); controller.cancel();
self.progress_operations.remove(&id); self.progress_operations.remove(id);
} }
} }
Message::PendingComplete(id, op_sel) => { Message::PendingComplete(id, op_sel) => {
@ -2545,8 +2564,6 @@ impl Application for App {
commands.push(self.rescan_operation_selection(op_sel)); commands.push(self.rescan_operation_selection(op_sel));
// Manually rescan any trash tabs after any operation is completed // Manually rescan any trash tabs after any operation is completed
commands.push(self.rescan_trash()); commands.push(self.rescan_trash());
// if search is active, update "search" tab view
commands.push(self.search());
return Task::batch(commands); return Task::batch(commands);
} }
Message::PendingDismiss => { Message::PendingDismiss => {
@ -2603,12 +2620,14 @@ impl Application for App {
let selected_paths = self.selected_paths(entity_opt); let selected_paths = self.selected_paths(entity_opt);
let mut commands = Vec::with_capacity(selected_paths.len()); let mut commands = Vec::with_capacity(selected_paths.len());
for path in selected_paths { for path in selected_paths {
let mut settings = window::Settings::default(); let mut settings = window::Settings {
settings.decorations = true; decorations: true,
settings.min_size = Some(Size::new(360.0, 180.0)); min_size: Some(Size::new(360.0, 180.0)),
settings.resizable = true; resizable: true,
settings.size = Size::new(480.0, 600.0); size: Size::new(480.0, 600.0),
settings.transparent = true; transparent: true,
..Default::default()
};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -2869,7 +2888,7 @@ impl Application for App {
commands.push(Task::batch([ commands.push(Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(entity, tab_path, selection_paths), self.update_tab(entity, tab_path, selection_paths),
])); ]));
} }
tab::Command::DropFiles(to, from) => { tab::Command::DropFiles(to, from) => {
@ -2883,9 +2902,11 @@ impl Application for App {
App::exec_entry_action(entry, action); App::exec_entry_action(entry, action);
} }
tab::Command::Iced(iced_command) => { tab::Command::Iced(iced_command) => {
commands.push(iced_command.0.map(move |tab_message| { commands.push(
message::app(Message::TabMessage(Some(entity), tab_message)) iced_command.0.map(move |x| {
})); message::app(Message::TabMessage(Some(entity), x))
}),
);
} }
tab::Command::MoveToTrash(paths) => { tab::Command::MoveToTrash(paths) => {
self.operation(Operation::Delete { paths }); self.operation(Operation::Delete { paths });
@ -2920,6 +2941,10 @@ impl Application for App {
self.context_page = ContextPage::Preview(Some(entity), kind); self.context_page = ContextPage::Preview(Some(entity), kind);
self.set_show_context(true); self.set_show_context(true);
} }
tab::Command::SetOpenWith(mime, id) => {
//TODO: this will block for a few ms, run in background?
self.mime_app_cache.set_default(mime, id);
}
tab::Command::WindowDrag => { tab::Command::WindowDrag => {
if let Some(window_id) = &self.window_id_opt { if let Some(window_id) = &self.window_id_opt {
commands.push(window::drag(*window_id)); commands.push(window::drag(*window_id));
@ -2943,8 +2968,7 @@ impl Application for App {
return self.open_tab(location, true, None); return self.open_tab(location, true, None);
} }
Message::TabRescan(entity, location, parent_item_opt, items, selection_paths) => { Message::TabRescan(entity, location, parent_item_opt, items, selection_paths) => {
match self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
Some(tab) => {
if location == tab.location { if location == tab.location {
tab.parent_item_opt = parent_item_opt; tab.parent_item_opt = parent_item_opt;
tab.set_items(items); tab.set_items(items);
@ -2953,8 +2977,6 @@ impl Application for App {
} }
} }
} }
_ => (),
}
} }
Message::TabView(entity_opt, view) => { Message::TabView(entity_opt, view) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
@ -3024,6 +3046,12 @@ impl Application for App {
]); ]);
} }
} }
Message::WindowUnfocus => {
let tab_entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {
tab.context_menu = None;
}
}
Message::WindowCloseRequested(id) => { Message::WindowCloseRequested(id) => {
self.remove_window(&id); self.remove_window(&id);
} }
@ -3156,7 +3184,7 @@ impl Application for App {
return Task::batch([ return Task::batch([
self.update_title(), self.update_title(),
self.update_watcher(), self.update_watcher(),
self.rescan_tab(entity, location, None), self.update_tab(entity, location, None),
]); ]);
} }
} }
@ -3230,45 +3258,40 @@ impl Application for App {
// Applies selected nav bar context menu operation. // Applies selected nav bar context menu operation.
Message::NavMenuAction(action) => match action { Message::NavMenuAction(action) => match action {
NavMenuAction::Open(entity) => { NavMenuAction::Open(entity) => {
match self if let Some(path) = self
.nav_model .nav_model
.data::<Location>(entity) .data::<Location>(entity)
.and_then(|x| x.path_opt()) .and_then(|x| x.path_opt())
.map(|x| x.to_path_buf()) .map(|x| x.to_path_buf())
{ {
Some(path) => {
self.open_file(&path); self.open_file(&path);
} }
None => {}
}
} }
NavMenuAction::OpenWith(entity) => { NavMenuAction::OpenWith(entity) => {
match self if let Some(path) = self
.nav_model .nav_model
.data::<Location>(entity) .data::<Location>(entity)
.and_then(|x| x.path_opt()) .and_then(|x| x.path_opt())
.map(|x| x.to_path_buf()) .map(|x| x.to_path_buf())
{ {
Some(path) => match tab::item_from_path(&path, IconSizes::default()) { match tab::item_from_path(&path, IconSizes::default()) {
Ok(item) => { Ok(item) => {
return self.update(Message::DialogPush(DialogPage::OpenWith { return self.update(Message::DialogPush(DialogPage::OpenWith {
path: path.to_path_buf(), path: path.to_path_buf(),
mime: item.mime.clone(), mime: item.mime.clone(),
apps: item.open_with.clone(),
selected: 0, selected: 0,
store_opt: "x-scheme-handler/mime" store_opt: "x-scheme-handler/mime"
.parse::<mime_guess::Mime>() .parse::<mime_guess::Mime>()
.ok() .ok()
.and_then(|mime| { .and_then(|mime| {
mime_app::mime_apps(&mime).first().cloned() self.mime_app_cache.get(&mime).first().cloned()
}), }),
})); }));
} }
Err(err) => { Err(err) => {
log::warn!("failed to get item for path {:?}: {}", path, err); log::warn!("failed to get item for path {:?}: {}", path, err);
} }
}, }
None => {}
} }
} }
NavMenuAction::OpenInNewTab(entity) => { NavMenuAction::OpenInNewTab(entity) => {
@ -3285,8 +3308,7 @@ impl Application for App {
// Open the selected path in a new cosmic-files window. // Open the selected path in a new cosmic-files window.
NavMenuAction::OpenInNewWindow(entity) => { NavMenuAction::OpenInNewWindow(entity) => {
if let Some(&Location::Path(ref path)) = self.nav_model.data::<Location>(entity) if let Some(Location::Path(path)) = self.nav_model.data::<Location>(entity) {
{
match env::current_exe() { match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).arg(path).spawn() { Ok(exe) => match process::Command::new(&exe).arg(path).spawn() {
Ok(_child) => {} Ok(_child) => {}
@ -3347,8 +3369,9 @@ impl Application for App {
log::info!("output {}: created", output.id()); log::info!("output {}: created", output.id());
let surface_id = WindowId::unique(); let surface_id = WindowId::unique();
match self.surface_ids.insert(output.clone(), surface_id) { if let Some(old_surface_id) =
Some(old_surface_id) => { self.surface_ids.insert(output.clone(), surface_id)
{
//TODO: remove old surface? //TODO: remove old surface?
log::warn!( log::warn!(
"output {}: already had surface ID {:?}", "output {}: already had surface ID {:?}",
@ -3356,8 +3379,6 @@ impl Application for App {
old_surface_id old_surface_id
); );
} }
None => {}
}
let display = match output_info_opt { let display = match output_info_opt {
Some(output_info) => match output_info.name { Some(output_info) => match output_info.name {
@ -3426,7 +3447,7 @@ impl Application for App {
} }
Message::Cosmic(cosmic) => { Message::Cosmic(cosmic) => {
// Forward cosmic messages // Forward cosmic messages
return Task::perform(async move { cosmic }, |cosmic| message::cosmic(cosmic)); return Task::perform(async move { cosmic }, message::cosmic);
} }
Message::None => {} Message::None => {}
#[cfg(all(feature = "desktop", feature = "wayland"))] #[cfg(all(feature = "desktop", feature = "wayland"))]
@ -3502,14 +3523,17 @@ impl Application for App {
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
actions.extend(item.preview_header()) actions.extend(item.preview_header().into_iter().map(|element| {
element.map(move |x| Message::TabMessage(Some(entity), x))
}));
} }
} }
} }
}; };
context_drawer::context_drawer( context_drawer::context_drawer(
self.preview(entity_opt, kind, true), self.preview(entity_opt, kind, true)
Message::ToggleContextPage(ContextPage::Preview(*entity_opt, kind.clone())), .map(move |x| Message::TabMessage(Some(entity), x)),
Message::ToggleContextPage(ContextPage::Preview(Some(entity), kind.clone())),
) )
.header_actions(actions) .header_actions(actions)
} }
@ -3528,7 +3552,7 @@ impl Application for App {
if tab.gallery { if tab.gallery {
return Some( return Some(
tab.gallery_view() tab.gallery_view()
.map(move |tab_message| Message::TabMessage(Some(entity), tab_message)), .map(move |x| Message::TabMessage(Some(entity), x)),
); );
} }
} }
@ -3666,10 +3690,11 @@ impl Application for App {
} => { } => {
//TODO: use URI! //TODO: use URI!
let mut controls = Vec::with_capacity(4); let mut controls = Vec::with_capacity(4);
let mut id_assigned = false;
if let Some(username) = &auth.username_opt { if let Some(username) = &auth.username_opt {
//TODO: what should submit do? //TODO: what should submit do?
controls.push( let mut input = widget::text_input(fl!("username"), username)
widget::text_input(fl!("username"), username)
.on_input(move |value| { .on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth { Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key, mounter_key: *mounter_key,
@ -3681,14 +3706,17 @@ impl Application for App {
auth_tx: auth_tx.clone(), auth_tx: auth_tx.clone(),
}) })
}) })
.on_submit(Message::DialogComplete) .on_submit(Message::DialogComplete);
.into(), if !id_assigned {
); input = input.id(self.dialog_text_input.clone());
id_assigned = true;
} }
controls.push(input.into());
}
if let Some(domain) = &auth.domain_opt { if let Some(domain) = &auth.domain_opt {
//TODO: what should submit do? //TODO: what should submit do?
controls.push( let mut input = widget::text_input(fl!("domain"), domain)
widget::text_input(fl!("domain"), domain)
.on_input(move |value| { .on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth { Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key, mounter_key: *mounter_key,
@ -3700,15 +3728,18 @@ impl Application for App {
auth_tx: auth_tx.clone(), auth_tx: auth_tx.clone(),
}) })
}) })
.on_submit(Message::DialogComplete) .on_submit(Message::DialogComplete);
.into(), if !id_assigned {
); input = input.id(self.dialog_text_input.clone());
id_assigned = true;
} }
controls.push(input.into());
}
if let Some(password) = &auth.password_opt { if let Some(password) = &auth.password_opt {
//TODO: what should submit do? //TODO: what should submit do?
//TODO: button for showing password //TODO: button for showing password
controls.push( let mut input = widget::secure_input(fl!("password"), password, None, true)
widget::secure_input(fl!("password"), password, None, true)
.on_input(move |value| { .on_input(move |value| {
Message::DialogUpdate(DialogPage::NetworkAuth { Message::DialogUpdate(DialogPage::NetworkAuth {
mounter_key: *mounter_key, mounter_key: *mounter_key,
@ -3720,10 +3751,13 @@ impl Application for App {
auth_tx: auth_tx.clone(), auth_tx: auth_tx.clone(),
}) })
}) })
.on_submit(Message::DialogComplete) .on_submit(Message::DialogComplete);
.into(), if !id_assigned {
); input = input.id(self.dialog_text_input.clone());
} }
controls.push(input.into());
}
if let Some(remember) = &auth.remember_opt { if let Some(remember) = &auth.remember_opt {
//TODO: what should submit do? //TODO: what should submit do?
//TODO: button for showing password //TODO: button for showing password
@ -3747,7 +3781,8 @@ impl Application for App {
let mut parts = auth.message.splitn(2, '\n'); let mut parts = auth.message.splitn(2, '\n');
let title = parts.next().unwrap_or_default(); let title = parts.next().unwrap_or_default();
let body = parts.next().unwrap_or_default(); let body = parts.next().unwrap_or_default();
widget::dialog()
let mut widget = widget::dialog()
.title(title) .title(title)
.body(body) .body(body)
.control(widget::column::with_children(controls).spacing(space_s)) .control(widget::column::with_children(controls).spacing(space_s))
@ -3756,8 +3791,11 @@ 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),
) );
.tertiary_action(widget::button::text(fl!("connect-anonymously")).on_press(
if let Some(_anonymous) = &auth.anonymous_opt {
widget = widget.tertiary_action(
widget::button::text(fl!("connect-anonymously")).on_press(
Message::DialogUpdateComplete(DialogPage::NetworkAuth { Message::DialogUpdateComplete(DialogPage::NetworkAuth {
mounter_key: *mounter_key, mounter_key: *mounter_key,
uri: uri.clone(), uri: uri.clone(),
@ -3767,7 +3805,11 @@ impl Application for App {
}, },
auth_tx: auth_tx.clone(), auth_tx: auth_tx.clone(),
}), }),
)) ),
);
}
widget
} }
DialogPage::NetworkError { DialogPage::NetworkError {
mounter_key: _, mounter_key: _,
@ -3853,7 +3895,7 @@ impl Application for App {
} }
DialogPage::OpenWith { DialogPage::OpenWith {
path, path,
apps, mime,
selected, selected,
store_opt, store_opt,
.. ..
@ -3864,7 +3906,7 @@ impl Application for App {
}; };
let mut column = widget::list_column(); let mut column = widget::list_column();
for (i, app) in apps.iter().enumerate() { for (i, app) in self.mime_app_cache.get(mime).iter().enumerate() {
column = column.add( column = column.add(
widget::button::custom( widget::button::custom(
widget::row::with_children(vec![ widget::row::with_children(vec![
@ -3942,7 +3984,7 @@ impl Application for App {
None None
} else { } else {
let path = parent.join(name); let path = parent.join(name);
if path.exists() { if from != &path && path.exists() {
if path.is_dir() { if path.is_dir() {
dialog = dialog dialog = dialog
.tertiary_action(widget::text::body(fl!("folder-already-exists"))); .tertiary_action(widget::text::body(fl!("folder-already-exists")));
@ -4001,8 +4043,14 @@ impl Application for App {
let dialog = widget::dialog() let dialog = widget::dialog()
.title(fl!("replace-title", filename = to.name.as_str())) .title(fl!("replace-title", filename = to.name.as_str()))
.body(fl!("replace-warning-operation")) .body(fl!("replace-warning-operation"))
.control(to.replace_view(fl!("original-file"), IconSizes::default())) .control(
.control(from.replace_view(fl!("replace-with"), IconSizes::default())) to.replace_view(fl!("original-file"), IconSizes::default())
.map(|x| Message::TabMessage(None, x)),
)
.control(
from.replace_view(fl!("replace-with"), IconSizes::default())
.map(|x| Message::TabMessage(None, x)),
)
.primary_action(widget::button::suggested(fl!("replace")).on_press( .primary_action(widget::button::suggested(fl!("replace")).on_press(
Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)), Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)),
)); ));
@ -4098,7 +4146,7 @@ impl Application for App {
let running = count; let running = count;
// Adjust the progress bar so it does not jump around when operations finish // Adjust the progress bar so it does not jump around when operations finish
for id in self.progress_operations.iter() { for id in self.progress_operations.iter() {
if self.complete_operations.contains_key(&id) { if self.complete_operations.contains_key(id) {
total_progress += 1.0; total_progress += 1.0;
count += 1; count += 1;
} }
@ -4191,8 +4239,7 @@ impl Application for App {
self.tab_model.active_data::<Tab>(), self.tab_model.active_data::<Tab>(),
&self.config, &self.config,
&self.key_binds, &self.key_binds,
) )]
.into()]
} }
fn header_end(&self) -> Vec<Element<Self::Message>> { fn header_end(&self) -> Vec<Element<Self::Message>> {
@ -4321,16 +4368,16 @@ impl Application for App {
return if let Some(margin) = self.margin.get(&id) { return if let Some(margin) = self.margin.get(&id) {
if margin.0 >= 0. || margin.2 >= 0. { if margin.0 >= 0. || margin.2 >= 0. {
tab_column = widget::column::with_children(vec![ tab_column = widget::column::with_children(vec![
vertical_space().height(margin.0 as f32).into(), vertical_space().height(margin.0).into(),
tab_column.into(), tab_column.into(),
vertical_space().height(margin.2 as f32).into(), vertical_space().height(margin.2).into(),
]) ])
} }
if margin.1 >= 0. || margin.3 >= 0. { if margin.1 >= 0. || margin.3 >= 0. {
Element::from(widget::row::with_children(vec![ Element::from(widget::row::with_children(vec![
horizontal_space().width(margin.1 as f32).into(), horizontal_space().width(margin.1).into(),
tab_column.into(), tab_column.into(),
horizontal_space().width(margin.3 as f32).into(), horizontal_space().width(margin.3).into(),
])) ]))
} else { } else {
tab_column.into() tab_column.into()
@ -4340,7 +4387,9 @@ impl Application for App {
}; };
} }
Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(), Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(),
Some(WindowKind::Preview(entity_opt, kind)) => self.preview(entity_opt, kind, false), Some(WindowKind::Preview(entity_opt, kind)) => self
.preview(entity_opt, kind, false)
.map(|x| Message::TabMessage(*entity_opt, x)),
None => { None => {
//TODO: distinct views per monitor in desktop mode //TODO: distinct views per monitor in desktop mode
return self.view_main().map(|message| match message { return self.view_main().map(|message| match message {
@ -4372,6 +4421,7 @@ impl Application for App {
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
Some(Message::Modifiers(modifiers)) Some(Message::Modifiers(modifiers))
} }
Event::Window(WindowEvent::Unfocused) => Some(Message::WindowUnfocus),
Event::Window(WindowEvent::CloseRequested) => Some(Message::WindowClose), Event::Window(WindowEvent::CloseRequested) => Some(Message::WindowClose),
Event::Window(WindowEvent::Opened { position: _, size }) => { Event::Window(WindowEvent::Opened { position: _, size }) => {
Some(Message::Size(size)) Some(Message::Size(size))
@ -4695,7 +4745,6 @@ pub(crate) mod test_utils {
use crate::{ use crate::{
config::{IconSizes, TabConfig}, config::{IconSizes, TabConfig},
mounter::MounterMap,
tab::Item, tab::Item,
}; };
@ -4933,8 +4982,8 @@ pub(crate) mod test_utils {
assert!( assert!(
entries entries
.into_iter() .into_iter()
.zip(tab.items_opt().clone().unwrap_or(&empty)) .zip(tab.items_opt().unwrap_or(&empty))
.all(|(a, b)| eq_path_item(&a, &b)), .all(|(a, b)| eq_path_item(&a, b)),
"Path ({}) and Tab path ({}) don't have equal contents", "Path ({}) and Tab path ({}) don't have equal contents",
path.display(), path.display(),
tab_path.display() tab_path.display()

View file

@ -28,6 +28,8 @@ impl ClipboardCopy {
pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self { pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: &[P]) -> Self {
let available = vec![ let available = vec![
"text/plain".to_string(), "text/plain".to_string(),
"text/plain;charset=utf-8".to_string(),
"UTF8_STRING".to_string(),
"text/uri-list".to_string(), "text/uri-list".to_string(),
"x-special/gnome-copied-files".to_string(), "x-special/gnome-copied-files".to_string(),
]; ];
@ -94,7 +96,9 @@ impl AsMimeTypes for ClipboardCopy {
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> { fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
match mime_type { match mime_type {
"text/plain" => Some(self.text_plain.clone()), "text/plain" | "text/plain;charset=utf-8" | "UTF8_STRING" => {
Some(self.text_plain.clone())
}
"text/uri-list" => Some(self.text_uri_list.clone()), "text/uri-list" => Some(self.text_uri_list.clone()),
"x-special/gnome-copied-files" => Some(self.x_special_gnome_copied_files.clone()), "x-special/gnome-copied-files" => Some(self.x_special_gnome_copied_files.clone()),
_ => None, _ => None,

View file

@ -68,7 +68,7 @@ impl Favorite {
Self::Videos, Self::Videos,
] { ] {
if let Some(favorite_path) = favorite.path_opt() { if let Some(favorite_path) = favorite.path_opt() {
if &favorite_path == &path { if favorite_path == path {
return favorite.clone(); return favorite.clone();
} }
} }

View file

@ -157,13 +157,15 @@ impl<M: Send + 'static> Dialog<M> {
let (config_handler, config) = Config::load(); let (config_handler, config) = Config::load();
let mut settings = window::Settings::default(); let mut settings = window::Settings {
settings.decorations = false; decorations: false,
settings.exit_on_close_request = false; exit_on_close_request: false,
settings.min_size = Some(Size::new(360.0, 180.0)); min_size: Some(Size::new(360.0, 180.0)),
settings.resizable = true; resizable: true,
settings.size = Size::new(1024.0, 640.0); size: Size::new(1024.0, 640.0),
settings.transparent = true; transparent: true,
..Default::default()
};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -251,7 +253,8 @@ impl<M: Send + 'static> Dialog<M> {
self.cosmic self.cosmic
.subscription() .subscription()
.map(DialogMessage) .map(DialogMessage)
.map(self.mapper) .with(self.mapper)
.map(|(mapper, message)| mapper(message))
} }
pub fn update(&mut self, message: DialogMessage) -> Task<M> { pub fn update(&mut self, message: DialogMessage) -> Task<M> {
@ -295,6 +298,7 @@ struct Flags {
kind: DialogKind, kind: DialogKind,
path_opt: Option<PathBuf>, path_opt: Option<PathBuf>,
window_id: window::Id, window_id: window::Id,
#[allow(dead_code)]
config_handler: Option<cosmic_config::Config>, config_handler: Option<cosmic_config::Config>,
config: Config, config: Config,
} }
@ -323,6 +327,7 @@ enum Message {
SearchActivate, SearchActivate,
SearchClear, SearchClear,
SearchInput(String), SearchInput(String),
#[allow(clippy::enum_variant_names)]
TabMessage(tab::Message), TabMessage(tab::Message),
TabRescan(Location, Option<tab::Item>, Vec<tab::Item>), TabRescan(Location, Option<tab::Item>, Vec<tab::Item>),
TabView(tab::View), TabView(tab::View),
@ -344,6 +349,7 @@ impl From<AppMessage> for Message {
AppMessage::ZoomDefault(_entity_opt) => Message::ZoomDefault, AppMessage::ZoomDefault(_entity_opt) => Message::ZoomDefault,
AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn, AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn,
AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut, AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut,
AppMessage::NewItem(_entity_opt, true) => Message::NewFolder,
unsupported => { unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode"); log::warn!("{unsupported:?} not supported in dialog mode");
Message::None Message::None
@ -465,17 +471,17 @@ impl App {
.into() .into()
} }
fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, AppMessage> { fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> {
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
match kind { match kind {
PreviewKind::Custom(PreviewItem(item)) => { PreviewKind::Custom(PreviewItem(item)) => {
children.push(item.preview_view(IconSizes::default())); children.push(item.preview_view(None, IconSizes::default()));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.location_opt.as_ref() == Some(location) { if item.location_opt.as_ref() == Some(location) {
children.push(item.preview_view(self.tab.config.icon_sizes)); children.push(item.preview_view(None, self.tab.config.icon_sizes));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -487,7 +493,7 @@ impl App {
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.preview_view(self.tab.config.icon_sizes)); children.push(item.preview_view(None, self.tab.config.icon_sizes));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -495,7 +501,7 @@ impl App {
} }
if children.is_empty() { if children.is_empty() {
if let Some(item) = &self.tab.parent_item_opt { if let Some(item) = &self.tab.parent_item_opt {
children.push(item.preview_view(self.tab.config.icon_sizes)); children.push(item.preview_view(None, self.tab.config.icon_sizes));
} }
} }
} }
@ -596,7 +602,7 @@ impl App {
.data(Location::Recents) .data(Location::Recents)
}); });
for (_favorite_i, favorite) in self.flags.config.favorites.iter().enumerate() { for favorite in self.flags.config.favorites.iter() {
if let Some(path) = favorite.path_opt() { if let Some(path) = favorite.path_opt() {
let name = if matches!(favorite, Favorite::Home) { let name = if matches!(favorite, Favorite::Home) {
fl!("home") fl!("home")
@ -808,14 +814,14 @@ impl Application for App {
actions.extend( actions.extend(
item.preview_header() item.preview_header()
.into_iter() .into_iter()
.map(|element| element.map(Message::from)), .map(|element| element.map(Message::TabMessage)),
) )
} }
} }
}; };
Some( Some(
context_drawer::context_drawer( context_drawer::context_drawer(
self.preview(kind).map(Message::from), self.preview(kind).map(Message::TabMessage),
Message::Preview, Message::Preview,
) )
.header_actions(actions), .header_actions(actions),
@ -972,11 +978,8 @@ impl Application for App {
ContextPage::Preview(..) => self.core.window.show_context, ContextPage::Preview(..) => self.core.window.show_context,
_ => false, _ => false,
}; };
elements.push( elements
menu::dialog_menu(&self.tab, &self.key_binds, show_details) .push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from));
.map(Message::from)
.into(),
);
elements elements
} }
@ -1025,7 +1028,7 @@ impl Application for App {
return self.update(message); return self.update(message);
} }
if let Some(data) = self.nav_model.data::<MounterData>(entity).clone() { if let Some(data) = self.nav_model.data::<MounterData>(entity) {
if let Some(mounter) = MOUNTERS.get(&data.0) { if let Some(mounter) = MOUNTERS.get(&data.0) {
return mounter.mount(data.1.clone()).map(|_| message::none()); return mounter.mount(data.1.clone()).map(|_| message::none());
} }
@ -1167,14 +1170,12 @@ impl Application for App {
let mut still_mounted = false; let mut still_mounted = false;
for item in mounter_items.iter() { for item in mounter_items.iter() {
if let Some(path) = item.path() { if let Some(path) = item.path() {
if path == old_path { if path == old_path && item.is_mounted() {
if item.is_mounted() {
still_mounted = true; still_mounted = true;
break; break;
} }
} }
} }
}
if !still_mounted { if !still_mounted {
unmounted.push(Location::Path(old_path)); unmounted.push(Location::Path(old_path));
} }
@ -1219,7 +1220,7 @@ impl Application for App {
let mut contains_change = false; let mut contains_change = false;
for event in events.iter() { for event in events.iter() {
for event_path in event.paths.iter() { for event_path in event.paths.iter() {
if event_path.starts_with(&path) { if event_path.starts_with(path) {
match event.kind { match event.kind {
notify::EventKind::Modify( notify::EventKind::Modify(
notify::event::ModifyKind::Metadata(_), notify::event::ModifyKind::Metadata(_),
@ -1233,14 +1234,14 @@ impl Application for App {
for item in items.iter_mut() { for item in items.iter_mut() {
if item.path_opt() == Some(event_path) { if item.path_opt() == Some(event_path) {
//TODO: reload more, like mime types? //TODO: reload more, like mime types?
match fs::metadata(&event_path) { match fs::metadata(event_path) {
Ok(new_metadata) => { Ok(new_metadata) => {
match &mut item.metadata { if let ItemMetadata::Path {
ItemMetadata::Path {
metadata, metadata,
.. ..
} => *metadata = new_metadata, } = &mut item.metadata
_ => {} {
*metadata = new_metadata;
} }
} }
Err(err) => { Err(err) => {
@ -1320,13 +1321,10 @@ impl Application for App {
// If we are in directory mode, return the current directory // If we are in directory mode, return the current directory
if self.flags.kind.is_dir() { if self.flags.kind.is_dir() {
match &self.tab.location { if let Location::Path(tab_path) = &self.tab.location {
Location::Path(tab_path) => {
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()])); self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
return window::close(self.flags.window_id); return window::close(self.flags.window_id);
} }
_ => {}
}
} }
} }
Message::Preview => match self.context_page { Message::Preview => match self.context_page {
@ -1342,7 +1340,7 @@ impl Application for App {
if let DialogKind::SaveFile { filename } = &self.flags.kind { if let DialogKind::SaveFile { filename } = &self.flags.kind {
if !filename.is_empty() { if !filename.is_empty() {
if let Some(tab_path) = self.tab.location.path_opt() { if let Some(tab_path) = self.tab.location.path_opt() {
let path = tab_path.join(&filename); let path = tab_path.join(filename);
if path.is_dir() { if path.is_dir() {
// cd to directory // cd to directory
let message = Message::TabMessage(tab::Message::Location( let message = Message::TabMessage(tab::Message::Location(
@ -1590,11 +1588,7 @@ impl Application for App {
} }
} }
col = col.push( col = col.push(self.tab.view(&self.key_binds).map(Message::TabMessage));
self.tab
.view(&self.key_binds)
.map(move |message| Message::TabMessage(message)),
);
col.into() col.into()
} }
@ -1708,16 +1702,17 @@ impl Application for App {
]; ];
for (key, mounter) in MOUNTERS.iter() { for (key, mounter) in MOUNTERS.iter() {
let key = *key; subscriptions.push(
subscriptions.push(mounter.subscription().map(move |mounter_message| { mounter.subscription().with(*key).map(
match mounter_message { |(key, mounter_message)| match mounter_message {
MounterMessage::Items(items) => Message::MounterItems(key, items), MounterMessage::Items(items) => Message::MounterItems(key, items),
_ => { _ => {
log::warn!("{:?} not supported in dialog mode", mounter_message); log::warn!("{:?} not supported in dialog mode", mounter_message);
Message::None Message::None
} }
} },
})); ),
);
} }
Subscription::batch(subscriptions) Subscription::batch(subscriptions)

View file

@ -29,10 +29,14 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap<KeyBind, Action> {
bind!([], Key::Named(Named::ArrowLeft), ItemLeft); bind!([], Key::Named(Named::ArrowLeft), ItemLeft);
bind!([], Key::Named(Named::ArrowRight), ItemRight); bind!([], Key::Named(Named::ArrowRight), ItemRight);
bind!([], Key::Named(Named::ArrowUp), ItemUp); bind!([], Key::Named(Named::ArrowUp), ItemUp);
bind!([], Key::Named(Named::Home), SelectFirst);
bind!([], Key::Named(Named::End), SelectLast);
bind!([Shift], Key::Named(Named::ArrowDown), ItemDown); bind!([Shift], Key::Named(Named::ArrowDown), ItemDown);
bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft); bind!([Shift], Key::Named(Named::ArrowLeft), ItemLeft);
bind!([Shift], Key::Named(Named::ArrowRight), ItemRight); bind!([Shift], Key::Named(Named::ArrowRight), ItemRight);
bind!([Shift], Key::Named(Named::ArrowUp), ItemUp); bind!([Shift], Key::Named(Named::ArrowUp), ItemUp);
bind!([Shift], Key::Named(Named::Home), SelectFirst);
bind!([Shift], Key::Named(Named::End), SelectLast);
bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder); bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder);
bind!([], Key::Named(Named::Enter), Open); bind!([], Key::Named(Named::Enter), Open);
bind!([Ctrl], Key::Named(Named::Space), Preview); bind!([Ctrl], Key::Named(Named::Space), Preview);

View file

@ -98,10 +98,21 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
} else if &arg == "--trash" { } else if &arg == "--trash" {
Location::Trash Location::Trash
} else { } else {
match fs::canonicalize(&arg) { //TODO: support more URLs
let path = match url::Url::parse(&arg) {
Ok(url) => match url.to_file_path() {
Ok(path) => path,
Err(()) => {
log::warn!("invalid argument {:?}", arg);
continue;
}
},
Err(_) => PathBuf::from(arg),
};
match fs::canonicalize(&path) {
Ok(absolute) => Location::Path(absolute), Ok(absolute) => Location::Path(absolute),
Err(err) => { Err(err) => {
log::warn!("failed to canonicalize {:?}: {}", arg, err); log::warn!("failed to canonicalize {:?}: {}", path, err);
continue; continue;
} }
} }

View file

@ -89,7 +89,7 @@ pub fn context_menu<'a>(
let mut selected_trash_only = false; let mut selected_trash_only = false;
let mut selected_desktop_entry = None; let mut selected_desktop_entry = None;
let mut selected_types: Vec<Mime> = vec![]; let mut selected_types: Vec<Mime> = vec![];
tab.items_opt().map(|items| { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
selected += 1; selected += 1;
@ -110,7 +110,7 @@ pub fn context_menu<'a>(
selected_types.push(item.mime.clone()); selected_types.push(item.mime.clone());
} }
} }
}); };
selected_types.sort_unstable(); selected_types.sort_unstable();
selected_types.dedup(); selected_types.dedup();
selected_trash_only = selected_trash_only && selected == 1; selected_trash_only = selected_trash_only && selected == 1;
@ -157,13 +157,13 @@ pub fn context_menu<'a>(
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open).into());
} }
if selected == 1 { if selected == 1 {
children.push(menu_item(fl!("open-with"), Action::OpenWith).into()); children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into());
if selected_dir == 1 { if selected_dir == 1 {
children children
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); .push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
} }
} }
if matches!(tab.location, Location::Search(..)) { if matches!(tab.location, Location::Search(..) | Location::Recents) {
children.push( children.push(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
); );
@ -260,7 +260,7 @@ pub fn context_menu<'a>(
if selected_dir == 1 && selected == 1 || selected_dir == 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open).into());
} }
if matches!(tab.location, Location::Search(..)) { if matches!(tab.location, Location::Search(..) | Location::Recents) {
children.push( children.push(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(),
); );
@ -342,7 +342,7 @@ pub fn context_menu<'a>(
.into() .into()
} }
pub fn dialog_menu<'a>( pub fn dialog_menu(
tab: &Tab, tab: &Tab,
key_binds: &HashMap<KeyBind, Action>, key_binds: &HashMap<KeyBind, Action>,
show_details: bool, show_details: bool,
@ -359,15 +359,13 @@ pub fn dialog_menu<'a>(
let in_trash = tab.location == Location::Trash; let in_trash = tab.location == Location::Trash;
let mut selected_gallery = 0; let mut selected_gallery = 0;
tab.items_opt().map(|items| { if let Some(items) = tab.items_opt() {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected && item.can_gallery() {
if item.can_gallery() {
selected_gallery += 1; selected_gallery += 1;
} }
} }
} };
});
MenuBar::new(vec![ MenuBar::new(vec![
menu::Tree::with_children( menu::Tree::with_children(
@ -504,7 +502,7 @@ pub fn menu_bar<'a>(
let mut selected_dir = 0; let mut selected_dir = 0;
let mut selected = 0; let mut selected = 0;
let mut selected_gallery = 0; let mut selected_gallery = 0;
tab_opt.and_then(|tab| tab.items_opt()).map(|items| { if let Some(items) = tab_opt.and_then(|tab| tab.items_opt()) {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
selected += 1; selected += 1;
@ -516,7 +514,7 @@ pub fn menu_bar<'a>(
} }
} }
} }
}); };
MenuBar::new(vec![ MenuBar::new(vec![
menu::Tree::with_children( menu::Tree::with_children(
@ -533,7 +531,7 @@ pub fn menu_bar<'a>(
Action::Open, Action::Open,
(selected > 0 && selected_dir == 0) || (selected_dir == 1 && selected == 1), (selected > 0 && selected_dir == 0) || (selected_dir == 1 && selected == 1),
), ),
menu_button_optional(fl!("open-with"), Action::OpenWith, selected == 1), menu_button_optional(fl!("menu-open-with"), Action::OpenWith, selected == 1),
menu::Item::Divider, menu::Item::Divider,
menu_button_optional(fl!("rename"), Action::Rename, selected > 0), menu_button_optional(fl!("rename"), Action::Rename, selected > 0),
menu::Item::Divider, menu::Item::Divider,

View file

@ -5,9 +5,8 @@
use cosmic::desktop; use cosmic::desktop;
use cosmic::widget; use cosmic::widget;
pub use mime_guess::Mime; pub use mime_guess::Mime;
use once_cell::sync::Lazy;
use std::{ use std::{
cmp::Ordering, collections::HashMap, env, ffi::OsString, path::PathBuf, process, sync::Mutex, cmp::Ordering, collections::HashMap, env, ffi::OsString, fs, io, path::PathBuf, process,
time::Instant, time::Instant,
}; };
@ -52,6 +51,13 @@ impl MimeApp {
} }
} }
// This allows usage of MimeApp in a dropdown
impl AsRef<str> for MimeApp {
fn as_ref(&self) -> &str {
&self.name
}
}
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
impl From<&desktop::DesktopEntryData> for MimeApp { impl From<&desktop::DesktopEntryData> for MimeApp {
fn from(app: &desktop::DesktopEntryData) -> Self { fn from(app: &desktop::DesktopEntryData) -> Self {
@ -82,6 +88,7 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
pub struct MimeAppCache { pub struct MimeAppCache {
cache: HashMap<Mime, Vec<MimeApp>>, cache: HashMap<Mime, Vec<MimeApp>>,
icons: HashMap<Mime, Vec<widget::icon::Handle>>,
terminals: Vec<MimeApp>, terminals: Vec<MimeApp>,
} }
@ -89,6 +96,7 @@ impl MimeAppCache {
pub fn new() -> Self { pub fn new() -> Self {
let mut mime_app_cache = Self { let mut mime_app_cache = Self {
cache: HashMap::new(), cache: HashMap::new(),
icons: HashMap::new(),
terminals: Vec::new(), terminals: Vec::new(),
}; };
mime_app_cache.reload(); mime_app_cache.reload();
@ -106,6 +114,7 @@ impl MimeAppCache {
let start = Instant::now(); let start = Instant::now();
self.cache.clear(); self.cache.clear();
self.icons.clear();
self.terminals.clear(); self.terminals.clear();
//TODO: get proper locale? //TODO: get proper locale?
@ -120,7 +129,7 @@ impl MimeAppCache {
.cache .cache
.entry(mime.clone()) .entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1)); .or_insert_with(|| Vec::with_capacity(1));
if apps.iter().find(|x| x.id == app.id).is_none() { if !apps.iter().any(|x| x.id == app.id) {
apps.push(MimeApp::from(app)); apps.push(MimeApp::from(app));
} }
} }
@ -191,11 +200,7 @@ impl MimeAppCache {
.cache .cache
.entry(mime.clone()) .entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1)); .or_insert_with(|| Vec::with_capacity(1));
if apps if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
.iter()
.find(|x| filename_eq(&x.path, filename))
.is_none()
{
if let Some(app) = if let Some(app) =
all_apps.iter().find(|x| filename_eq(&x.path, filename)) all_apps.iter().find(|x| filename_eq(&x.path, filename))
{ {
@ -258,39 +263,94 @@ impl MimeAppCache {
}); });
} }
// Copy icons to special cache
//TODO: adjust dropdown API so this is no longer needed
for (mime, apps) in self.cache.iter() {
self.icons.insert(
mime.clone(),
apps.iter().map(|app| app.icon.clone()).collect(),
);
}
let elapsed = start.elapsed(); let elapsed = start.elapsed();
log::info!("loaded mime app cache in {:?}", elapsed); log::info!("loaded mime app cache in {:?}", elapsed);
} }
pub fn get(&self, key: &Mime) -> Vec<MimeApp> { pub fn get(&self, key: &Mime) -> &[MimeApp] {
self.cache static EMPTY: Vec<MimeApp> = Vec::new();
.get(&key) self.cache.get(key).unwrap_or(&EMPTY)
.map_or_else(|| Vec::new(), |x| x.clone())
}
} }
static MIME_APP_CACHE: Lazy<Mutex<MimeAppCache>> = Lazy::new(|| Mutex::new(MimeAppCache::new())); pub fn icons(&self, key: &Mime) -> &[widget::icon::Handle] {
static EMPTY: Vec<widget::icon::Handle> = Vec::new();
pub fn mime_apps(mime: &Mime) -> Vec<MimeApp> { self.icons.get(key).unwrap_or(&EMPTY)
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
mime_app_cache.get(mime)
} }
pub fn terminal() -> Option<MimeApp> { pub fn terminal(&self) -> Option<&MimeApp> {
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
// Look for and return preferred terminals // Look for and return preferred terminals
//TODO: fallback order beyond cosmic-term? //TODO: fallback order beyond cosmic-term?
for id in &["com.system76.CosmicTerm"] { for id in &["com.system76.CosmicTerm"] {
for terminal in mime_app_cache.terminals.iter() { for terminal in self.terminals.iter() {
if &terminal.id == id { if &terminal.id == id {
return Some(terminal.clone()); return Some(terminal);
} }
} }
} }
// Return whatever was the first terminal found // Return whatever was the first terminal found
mime_app_cache.terminals.first().map(|x| x.clone()) self.terminals.first()
}
#[cfg(not(feature = "desktop"))]
pub fn set_default(&mut self, mime: Mime, id: String) {
log::warn!(
"failed to set default handler for {mime:?} to {id:?}: desktop feature not enabled"
);
}
#[cfg(feature = "desktop")]
pub fn set_default(&mut self, mime: Mime, mut id: String) {
let Some(path) = cosmic_mime_apps::local_list_path() else {
log::warn!("failed to find mimeapps.list path");
return;
};
let mut list = cosmic_mime_apps::List::default();
match fs::read_to_string(&path) {
Ok(string) => {
list.load_from(&string);
}
Err(err) => {
if err.kind() != io::ErrorKind::NotFound {
log::warn!("failed to read {path:?}: {err}");
return;
}
}
}
let suffix = ".desktop";
if !id.ends_with(suffix) {
id.push_str(suffix);
}
list.set_default_app(mime, id);
let mut string = list.to_string();
string.push('\n');
match fs::write(&path, string) {
Ok(()) => {
self.reload();
}
Err(err) => {
log::warn!("failed to write {path:?}: {err}");
}
}
}
}
impl Default for MimeAppCache {
fn default() -> Self {
Self::new()
}
} }

View file

@ -97,7 +97,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
info.icon() info.icon()
.as_ref() .as_ref()
.and_then(|icon| gio_icon_to_path(icon, size)) .and_then(|icon| gio_icon_to_path(icon, size))
.map(|path| widget::icon::from_path(path)) .map(widget::icon::from_path)
.unwrap_or( .unwrap_or(
widget::icon::from_name(if metadata.is_dir() { widget::icon::from_name(if metadata.is_dir() {
"folder" "folder"
@ -127,7 +127,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -388,10 +387,7 @@ impl Gvfs {
let file = gio::File::for_uri(&uri); let file = gio::File::for_uri(&uri);
let needs_mount = match file.find_enclosing_mount(gio::Cancellable::NONE) { let needs_mount = match file.find_enclosing_mount(gio::Cancellable::NONE) {
Ok(_) => false, Ok(_) => false,
Err(err) => match err.kind::<gio::IOErrorEnum>() { Err(err) => matches!(err.kind::<gio::IOErrorEnum>(), Some(gio::IOErrorEnum::NotMounted))
Some(gio::IOErrorEnum::NotMounted) => true,
_ => false
}
}; };
if needs_mount { if needs_mount {
let mount_op = mount_op(uri.clone(), event_tx.clone()); let mount_op = mount_op(uri.clone(), event_tx.clone());
@ -468,7 +464,6 @@ impl Mounter for Gvfs {
Task::perform( Task::perform(
async move { async move {
command_tx.send(Cmd::Mount(item)).unwrap(); command_tx.send(Cmd::Mount(item)).unwrap();
()
}, },
|x| x, |x| x,
) )
@ -479,7 +474,6 @@ impl Mounter for Gvfs {
Task::perform( Task::perform(
async move { async move {
command_tx.send(Cmd::NetworkDrive(uri)).unwrap(); command_tx.send(Cmd::NetworkDrive(uri)).unwrap();
()
}, },
|x| x, |x| x,
) )
@ -498,7 +492,6 @@ impl Mounter for Gvfs {
Task::perform( Task::perform(
async move { async move {
command_tx.send(Cmd::Unmount(item)).unwrap(); command_tx.send(Cmd::Unmount(item)).unwrap();
()
}, },
|x| x, |x| x,
) )

View file

@ -117,4 +117,4 @@ pub fn mounters() -> Mounters {
Mounters::new(mounters) Mounters::new(mounters)
} }
pub static MOUNTERS: Lazy<Mounters> = Lazy::new(|| mounters()); pub static MOUNTERS: Lazy<Mounters> = Lazy::new(mounters);

View file

@ -31,156 +31,150 @@ use crate::tab::DOUBLE_CLICK_DURATION;
pub struct MouseArea<'a, Message> { pub struct MouseArea<'a, Message> {
id: Id, id: Id,
content: Element<'a, Message>, content: Element<'a, Message>,
on_drag: Option<Box<dyn Fn(Option<Rectangle>) -> Message + 'a>>, on_drag: Option<Box<dyn OnDrag<'a, Message>>>,
on_double_click: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_double_click: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_drag_end: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_drag_end: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>, on_resize: Option<Box<dyn OnResize<'a, Message>>>,
on_right_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_right_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_right_press_no_capture: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_right_press_no_capture: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_right_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_right_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_middle_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_middle_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_middle_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_middle_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_back_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_back_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_back_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_back_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_forward_press: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_forward_press: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_forward_release: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>, on_forward_release: Option<Box<dyn OnMouseButton<'a, Message>>>,
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a>>, on_scroll: Option<Box<dyn OnScroll<'a, Message>>>,
on_enter: Option<Box<dyn Fn() -> Message + 'a>>, on_enter: Option<Box<dyn OnEnterExit<'a, Message>>>,
on_exit: Option<Box<dyn Fn() -> Message + 'a>>, on_exit: Option<Box<dyn OnEnterExit<'a, Message>>>,
show_drag_rect: bool, show_drag_rect: bool,
} }
impl<'a, Message> MouseArea<'a, Message> { impl<'a, Message> MouseArea<'a, Message> {
/// The message to emit when a drag is initiated. /// The message to emit when a drag is initiated.
#[must_use] #[must_use]
pub fn on_drag(mut self, message: impl Fn(Option<Rectangle>) -> Message + 'a) -> Self { pub fn on_drag(mut self, message: impl OnDrag<'a, Message>) -> Self {
self.on_drag = Some(Box::new(message)); self.on_drag = Some(Box::new(message));
self self
} }
/// The message to emit when a drag ends. /// The message to emit when a drag ends.
#[must_use] #[must_use]
pub fn on_drag_end(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_drag_end(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_drag_end = Some(Box::new(message)); self.on_drag_end = Some(Box::new(message));
self self
} }
/// The message to emit on a double click. /// The message to emit on a double click.
#[must_use] #[must_use]
pub fn on_double_click(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_double_click(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_double_click = Some(Box::new(message)); self.on_double_click = Some(Box::new(message));
self self
} }
/// The message to emit on a left button press. /// The message to emit on a left button press.
#[must_use] #[must_use]
pub fn on_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_press = Some(Box::new(message)); self.on_press = Some(Box::new(message));
self self
} }
/// The message to emit on a left button release. /// The message to emit on a left button release.
#[must_use] #[must_use]
pub fn on_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_release = Some(Box::new(message)); self.on_release = Some(Box::new(message));
self self
} }
/// The message to emit on resizing. /// The message to emit on resizing.
#[must_use] #[must_use]
pub fn on_resize(mut self, message: impl Fn(Size) -> Message + 'a) -> Self { pub fn on_resize(mut self, message: impl OnResize<'a, Message>) -> Self {
self.on_resize = Some(Box::new(message)); self.on_resize = Some(Box::new(message));
self self
} }
/// The message to emit on a right button press. /// The message to emit on a right button press.
#[must_use] #[must_use]
pub fn on_right_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_right_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_right_press = Some(Box::new(message)); self.on_right_press = Some(Box::new(message));
self self
} }
/// The message to emit on a right button press without capturing. /// The message to emit on a right button press without capturing.
#[must_use] #[must_use]
pub fn on_right_press_no_capture( pub fn on_right_press_no_capture(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
mut self,
message: impl Fn(Option<Point>) -> Message + 'a,
) -> Self {
self.on_right_press_no_capture = Some(Box::new(message)); self.on_right_press_no_capture = Some(Box::new(message));
self self
} }
/// The message to emit on a right button release. /// The message to emit on a right button release.
#[must_use] #[must_use]
pub fn on_right_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_right_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_right_release = Some(Box::new(message)); self.on_right_release = Some(Box::new(message));
self self
} }
/// The message to emit on a middle button press. /// The message to emit on a middle button press.
#[must_use] #[must_use]
pub fn on_middle_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_middle_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_middle_press = Some(Box::new(message)); self.on_middle_press = Some(Box::new(message));
self self
} }
/// The message to emit on a middle button release. /// The message to emit on a middle button release.
#[must_use] #[must_use]
pub fn on_middle_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_middle_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_middle_release = Some(Box::new(message)); self.on_middle_release = Some(Box::new(message));
self self
} }
/// The message to emit on a back button press. /// The message to emit on a back button press.
#[must_use] #[must_use]
pub fn on_back_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_back_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_back_press = Some(Box::new(message)); self.on_back_press = Some(Box::new(message));
self self
} }
/// The message to emit on a back button release. /// The message to emit on a back button release.
#[must_use] #[must_use]
pub fn on_back_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_back_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_back_release = Some(Box::new(message)); self.on_back_release = Some(Box::new(message));
self self
} }
/// The message to emit on a forward button press. /// The message to emit on a forward button press.
#[must_use] #[must_use]
pub fn on_forward_press(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_forward_press(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_forward_press = Some(Box::new(message)); self.on_forward_press = Some(Box::new(message));
self self
} }
/// The message to emit on a forward button release. /// The message to emit on a forward button release.
#[must_use] #[must_use]
pub fn on_forward_release(mut self, message: impl Fn(Option<Point>) -> Message + 'a) -> Self { pub fn on_forward_release(mut self, message: impl OnMouseButton<'a, Message>) -> Self {
self.on_forward_release = Some(Box::new(message)); self.on_forward_release = Some(Box::new(message));
self self
} }
/// The message to emit on a scroll. /// The message to emit on a scroll.
#[must_use] #[must_use]
pub fn on_scroll( pub fn on_scroll(mut self, message: impl OnScroll<'a, Message>) -> Self {
mut self,
message: impl Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a,
) -> Self {
self.on_scroll = Some(Box::new(message)); self.on_scroll = Some(Box::new(message));
self self
} }
/// The message to emit when a mouse enters the area. /// The message to emit when a mouse enters the area.
#[must_use] #[must_use]
pub fn on_enter(mut self, message: impl Fn() -> Message + 'a) -> Self { pub fn on_enter(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
self.on_enter = Some(Box::new(message)); self.on_enter = Some(Box::new(message));
self self
} }
/// The message to emit when a mouse exits the area. /// The message to emit when a mouse exits the area.
#[must_use] #[must_use]
pub fn on_exit(mut self, message: impl Fn() -> Message + 'a) -> Self { pub fn on_exit(mut self, message: impl OnEnterExit<'a, Message>) -> Self {
self.on_exit = Some(Box::new(message)); self.on_exit = Some(Box::new(message));
self self
} }
@ -199,6 +193,24 @@ impl<'a, Message> MouseArea<'a, Message> {
} }
} }
pub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {}
impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {}
pub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Message + 'a {}
impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Message + 'a {}
pub trait OnResize<'a, Message>: Fn(Size) -> Message + 'a {}
impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Size) -> Message + 'a {}
pub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a {}
impl<'a, Message, F> OnScroll<'a, Message> for F where
F: Fn(mouse::ScrollDelta, Modifiers) -> Option<Message> + 'a
{
}
pub trait OnEnterExit<'a, Message>: Fn() -> Message + 'a {}
impl<'a, Message, F> OnEnterExit<'a, Message> for F where F: Fn() -> Message + 'a {}
/// Local state of the [`MouseArea`]. /// Local state of the [`MouseArea`].
#[derive(Default)] #[derive(Default)]
struct State { struct State {
@ -250,7 +262,7 @@ impl State {
} else { } else {
mouse::Click::new(pos, mouse::Button::Left, None) mouse::Click::new(pos, mouse::Button::Left, None)
}; };
self.prev_click = Some((new.clone(), now)); self.prev_click = Some((new, now));
new new
} }
} }
@ -284,7 +296,7 @@ impl<'a, Message> MouseArea<'a, Message> {
} }
} }
impl<'a, Message> Widget<Message, Theme, Renderer> for MouseArea<'a, Message> impl<Message> Widget<Message, Theme, Renderer> for MouseArea<'_, Message>
where where
Message: Clone, Message: Clone,
{ {
@ -659,7 +671,7 @@ fn update<Message: Clone>(
if let Some(on_scroll) = widget.on_scroll.as_ref() { if let Some(on_scroll) = widget.on_scroll.as_ref() {
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
if let Some(message) = on_scroll(delta.clone(), state.modifiers) { if let Some(message) = on_scroll(*delta, state.modifiers) {
shell.publish(message); shell.publish(message);
return event::Status::Captured; return event::Status::Captured;
} }

View file

@ -22,8 +22,8 @@ pub struct Controller {
inner: Arc<ControllerInner>, inner: Arc<ControllerInner>,
} }
impl Controller { impl Default for Controller {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
primary: true, primary: true,
inner: Arc::new(ControllerInner { inner: Arc::new(ControllerInner {
@ -33,7 +33,9 @@ impl Controller {
}), }),
} }
} }
}
impl Controller {
pub fn check(&self) -> Result<(), String> { pub fn check(&self) -> Result<(), String> {
let mut state = self.inner.state.lock().unwrap(); let mut state = self.inner.state.lock().unwrap();
loop { loop {

View file

@ -67,11 +67,20 @@ fn handle_replace(
} }
fn get_directory_name(file_name: &str) -> &str { fn get_directory_name(file_name: &str) -> &str {
const SUPPORTED_EXTENSIONS: [&str; 4] = [".tar.gz", ".tgz", ".tar", ".zip"]; // TODO: Chain with COMPOUND_EXTENSIONS once more formats are supported
const SUPPORTED_EXTENSIONS: &[&str] = &[
".tar.bz2",
".tar.gz",
".tar.lzma",
".tar.xz",
".tgz",
".tar",
".zip",
];
for ext in &SUPPORTED_EXTENSIONS { for ext in SUPPORTED_EXTENSIONS {
if file_name.ends_with(ext) { if let Some(stripped) = file_name.strip_suffix(ext) {
return &file_name[..file_name.len() - ext.len()]; return stripped;
} }
} }
file_name file_name
@ -245,7 +254,7 @@ async fn copy_or_move(
if matches!(from.parent(), Some(parent) if parent == to) && !moving { if matches!(from.parent(), Some(parent) if parent == to) && !moving {
// `from`'s parent is equal to `to` which means we're copying to the same // `from`'s parent is equal to `to` which means we're copying to the same
// directory (duplicating files) // directory (duplicating files)
let to = copy_unique_path(&from, &to); let to = copy_unique_path(&from, to);
Some((from, to)) Some((from, to))
} else if let Some(name) = from.file_name() { } else if let Some(name) = from.file_name() {
let to = to.join(name); let to = to.join(name);
@ -361,12 +370,12 @@ fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
to to
} }
fn file_name<'a>(path: &'a Path) -> Cow<'a, str> { fn file_name(path: &Path) -> Cow<'_, str> {
path.file_name() path.file_name()
.map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy()) .map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy())
} }
fn parent_name<'a>(path: &'a Path) -> Cow<'a, str> { fn parent_name(path: &Path) -> Cow<'_, str> {
let Some(parent) = path.parent() else { let Some(parent) = path.parent() else {
return fl!("unknown-folder").into(); return fl!("unknown-folder").into();
}; };
@ -374,7 +383,7 @@ fn parent_name<'a>(path: &'a Path) -> Cow<'a, str> {
file_name(parent) file_name(parent)
} }
fn paths_parent_name<'a>(paths: &'a Vec<PathBuf>) -> Cow<'a, str> { fn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> {
let Some(first_path) = paths.first() else { let Some(first_path) = paths.first() else {
return fl!("unknown-folder").into(); return fl!("unknown-folder").into();
}; };
@ -674,7 +683,7 @@ impl Operation {
path.strip_prefix(relative_root).map_err(err_str)?.to_str() path.strip_prefix(relative_root).map_err(err_str)?.to_str()
{ {
if path.is_file() { if path.is_file() {
let mut file = fs::File::open(&path).map_err(err_str)?; let mut file = fs::File::open(path).map_err(err_str)?;
let metadata = file.metadata().map_err(err_str)?; let metadata = file.metadata().map_err(err_str)?;
let total = metadata.len(); let total = metadata.len();
if total >= 4 * 1024 * 1024 * 1024 { if total >= 4 * 1024 * 1024 * 1024 {
@ -777,8 +786,6 @@ impl Operation {
controller.set_progress((i as f32) / total_paths as f32); controller.set_progress((i as f32) / total_paths as f32);
let to = to.to_owned();
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
let dir_name = get_directory_name(file_name); let dir_name = get_directory_name(file_name);
let mut new_dir = to.join(dir_name); let mut new_dir = to.join(dir_name);
@ -793,7 +800,7 @@ impl Operation {
op_sel.selected.push(new_dir.clone()); op_sel.selected.push(new_dir.clone());
let controller = controller.clone(); let controller = controller.clone();
let mime = mime_for_path(&path); let mime = mime_for_path(path);
match mime.essence_str() { match mime.essence_str() {
"application/gzip" | "application/x-compressed-tar" => { "application/gzip" | "application/x-compressed-tar" => {
OpReader::new(path, controller) OpReader::new(path, controller)
@ -953,7 +960,7 @@ mod tests {
}; };
use cosmic::iced::futures::{channel::mpsc, StreamExt}; use cosmic::iced::futures::{channel::mpsc, StreamExt};
use log::{debug, trace}; use log::debug;
use test_log::test; use test_log::test;
use tokio::sync; use tokio::sync;
@ -961,8 +968,8 @@ mod tests {
use crate::{ use crate::{
app::{ app::{
test_utils::{ test_utils::{
empty_fs, filter_dirs, filter_files, read_dir_sorted, simple_fs, NAME_LEN, empty_fs, filter_dirs, filter_files, simple_fs, NAME_LEN, NUM_DIRS, NUM_FILES,
NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, NUM_HIDDEN, NUM_NESTED,
}, },
DialogPage, Message, DialogPage, Message,
}, },
@ -986,7 +993,7 @@ mod tests {
paths: paths_clone, paths: paths_clone,
to: to_clone, to: to_clone,
} }
.perform(&sync::Mutex::new(tx).into(), Controller::new()) .perform(&sync::Mutex::new(tx).into(), Controller::default())
.await .await
}); });

View file

@ -12,12 +12,18 @@ use super::{copy_unique_path, Controller, OperationSelection, ReplaceResult};
pub struct Context { pub struct Context {
buf: Vec<u8>, buf: Vec<u8>,
controller: Controller, controller: Controller,
on_progress: Box<dyn Fn(&Op, &Progress) + 'static>, on_progress: Box<dyn OnProgress>,
on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>, on_replace: Box<dyn OnReplace>,
pub(crate) op_sel: OperationSelection, pub(crate) op_sel: OperationSelection,
replace_result_opt: Option<ReplaceResult>, replace_result_opt: Option<ReplaceResult>,
} }
pub trait OnProgress: Fn(&Op, &Progress) + 'static {}
impl<F> OnProgress for F where F: Fn(&Op, &Progress) + 'static {}
pub trait OnReplace: Fn(&Op) -> ReplaceResult + 'static {}
impl<F> OnReplace for F where F: Fn(&Op) -> ReplaceResult + 'static {}
impl Context { impl Context {
pub fn new(controller: Controller) -> Self { pub fn new(controller: Controller) -> Self {
Self { Self {
@ -67,7 +73,7 @@ impl Context {
OpKind::Symlink { target } OpKind::Symlink { target }
} else { } else {
//TODO: present dialog and allow continue //TODO: present dialog and allow continue
return Err(format!("{} is not a known file type", from.display()).into()); return Err(format!("{} is not a known file type", from.display()));
}; };
let to = if from == from_parent { let to = if from == from_parent {
// When copying a file, from matches from_parent, and to_parent must be used // When copying a file, from matches from_parent, and to_parent must be used
@ -130,12 +136,12 @@ impl Context {
Ok(true) Ok(true)
} }
pub fn on_progress<F: Fn(&Op, &Progress) + 'static>(mut self, f: F) -> Self { pub fn on_progress<F: OnProgress>(mut self, f: F) -> Self {
self.on_progress = Box::new(f); self.on_progress = Box::new(f);
self self
} }
pub fn on_replace<F: Fn(&Op) -> ReplaceResult + 'static>(mut self, f: F) -> Self { pub fn on_replace<F: OnReplace>(mut self, f: F) -> Self {
self.on_replace = Box::new(f); self.on_replace = Box::new(f);
self self
} }
@ -153,9 +159,7 @@ impl Context {
Ok(ControlFlow::Continue(op.to.clone())) Ok(ControlFlow::Continue(op.to.clone()))
} }
ReplaceResult::KeepBoth => match op.to.parent() { ReplaceResult::KeepBoth => match op.to.parent() {
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path( Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(&op.from, to_parent))),
&op.from, &to_parent,
))),
None => Err(format!("failed to get parent of {:?}", op.to).into()), None => Err(format!("failed to get parent of {:?}", op.to).into()),
}, },
ReplaceResult::Skip(apply_to_all) => { ReplaceResult::Skip(apply_to_all) => {
@ -216,7 +220,7 @@ impl Op {
let metadata = from_file.metadata()?; let metadata = from_file.metadata()?;
// Remove `to` if overwriting and it is an existing file // Remove `to` if overwriting and it is an existing file
if self.to.is_file() { if self.to.is_file() {
match ctx.replace(&self)? { match ctx.replace(self)? {
ControlFlow::Continue(to) => { ControlFlow::Continue(to) => {
self.to = to; self.to = to;
} }
@ -226,7 +230,7 @@ impl Op {
} }
} }
progress.total_bytes = Some(metadata.len()); progress.total_bytes = Some(metadata.len());
(ctx.on_progress)(&self, &progress); (ctx.on_progress)(self, &progress);
// This is atomic and ensures `to` is not created by any other process // This is atomic and ensures `to` is not created by any other process
let mut to_file = fs::OpenOptions::new() let mut to_file = fs::OpenOptions::new()
.create_new(true) .create_new(true)
@ -242,14 +246,14 @@ impl Op {
} }
to_file.write_all(&ctx.buf[..count])?; to_file.write_all(&ctx.buf[..count])?;
progress.current_bytes += count as u64; progress.current_bytes += count as u64;
(ctx.on_progress)(&self, &progress); (ctx.on_progress)(self, &progress);
} }
to_file.sync_all()?; to_file.sync_all()?;
} }
OpKind::Move => { OpKind::Move => {
// Remove `to` if overwriting and it is an existing file // Remove `to` if overwriting and it is an existing file
if self.to.is_file() { if self.to.is_file() {
match ctx.replace(&self)? { match ctx.replace(self)? {
ControlFlow::Continue(to) => { ControlFlow::Continue(to) => {
self.to = to; self.to = to;
} }
@ -289,7 +293,7 @@ impl Op {
OpKind::Symlink { ref target } => { OpKind::Symlink { ref target } => {
// Remove `to` if overwriting and it is an existing file // Remove `to` if overwriting and it is an existing file
if self.to.is_file() { if self.to.is_file() {
match ctx.replace(&self)? { match ctx.replace(self)? {
ControlFlow::Continue(to) => { ControlFlow::Continue(to) => {
self.to = to; self.to = to;
} }
@ -298,8 +302,18 @@ impl Op {
} }
} }
} }
//TODO: use OS-specific function #[cfg(unix)]
fs::soft_link(&target, &self.to)?; {
std::os::unix::fs::symlink(target, &self.to)?;
}
#[cfg(windows)]
{
if target.is_dir() {
std::os::windows::fs::symlink_dir(target, &self.to)?;
} else {
std::os::windows::fs::symlink_file(target, &self.to)?;
}
}
} }
} }
Ok(true) Ok(true)

View file

@ -58,14 +58,13 @@ use tokio::sync::mpsc;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::{ use crate::{
app::{self, Action, PreviewItem, PreviewKind}, app::{Action, PreviewItem, PreviewKind},
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID}, config::{DesktopConfig, IconSizes, TabConfig, ICON_SCALE_MAX, ICON_SIZE_GRID},
dialog::DialogKind, dialog::DialogKind,
fl, fl,
localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER}, localize::{LANGUAGE_CHRONO, LANGUAGE_SORTER},
menu, menu, mime_app,
mime_app::{mime_apps, MimeApp},
mime_icon::{mime_for_path, mime_icon}, mime_icon::{mime_for_path, mime_icon},
mounter::MOUNTERS, mounter::MOUNTERS,
mouse_area, mouse_area,
@ -459,8 +458,6 @@ pub fn item_from_entry(
} }
}; };
let open_with = mime_apps(&mime);
let children = if metadata.is_dir() { let children = if metadata.is_dir() {
//TODO: calculate children in the background (and make it cancellable?) //TODO: calculate children in the background (and make it cancellable?)
match fs::read_dir(&path) { match fs::read_dir(&path) {
@ -475,7 +472,7 @@ pub fn item_from_entry(
}; };
let dir_size = if metadata.is_dir() { let dir_size = if metadata.is_dir() {
DirSize::Calculating(Controller::new()) DirSize::Calculating(Controller::default())
} else { } else {
DirSize::NotDirectory DirSize::NotDirectory
}; };
@ -490,7 +487,6 @@ pub fn item_from_entry(
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with,
thumbnail_opt: None, thumbnail_opt: None,
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -720,7 +716,6 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -904,7 +899,6 @@ pub fn scan_desktop(
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
icon_handle_list_condensed, icon_handle_list_condensed,
open_with: Vec::new(),
thumbnail_opt: Some(ItemThumbnail::NotImage), thumbnail_opt: Some(ItemThumbnail::NotImage),
button_id: widget::Id::unique(), button_id: widget::Id::unique(),
pos_opt: Cell::new(None), pos_opt: Cell::new(None),
@ -1027,6 +1021,7 @@ pub enum Command {
OpenInNewWindow(PathBuf), OpenInNewWindow(PathBuf),
OpenTrash, OpenTrash,
Preview(PreviewKind), Preview(PreviewKind),
SetOpenWith(Mime, String),
WindowDrag, WindowDrag,
WindowToggleMaximize, WindowToggleMaximize,
} }
@ -1071,6 +1066,9 @@ pub enum Message {
SearchContext(Location, SearchContextWrapper), SearchContext(Location, SearchContextWrapper),
SearchReady(bool), SearchReady(bool),
SelectAll, SelectAll,
SelectFirst,
SelectLast,
SetOpenWith(Mime, String),
SetSort(HeadingOptions, bool), SetSort(HeadingOptions, bool),
Thumbnail(PathBuf, ItemThumbnail), Thumbnail(PathBuf, ItemThumbnail),
ToggleShowHidden, ToggleShowHidden,
@ -1316,7 +1314,6 @@ pub struct Item {
pub icon_handle_grid: widget::icon::Handle, pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle,
pub icon_handle_list_condensed: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle,
pub open_with: Vec<MimeApp>,
pub thumbnail_opt: Option<ItemThumbnail>, pub thumbnail_opt: Option<ItemThumbnail>,
pub button_id: widget::Id, pub button_id: widget::Id,
pub pos_opt: Cell<Option<(usize, usize)>>, pub pos_opt: Cell<Option<(usize, usize)>>,
@ -1341,7 +1338,7 @@ impl Item {
self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT self.mime.type_() == mime::IMAGE || self.mime.type_() == mime::TEXT
} }
fn preview<'a>(&'a self, sizes: IconSizes) -> Element<'a, app::Message> { fn preview<'a>(&'a self, sizes: IconSizes) -> Element<'a, Message> {
let spacing = cosmic::theme::active().cosmic().spacing; let spacing = cosmic::theme::active().cosmic().spacing;
// This loads the image only if thumbnailing worked // This loads the image only if thumbnailing worked
let icon = widget::icon::icon(self.icon_handle_grid.clone()) let icon = widget::icon::icon(self.icon_handle_grid.clone())
@ -1374,23 +1371,23 @@ impl Item {
} }
} }
pub fn preview_header(&self) -> Vec<Element<app::Message>> { pub fn preview_header(&self) -> Vec<Element<Message>> {
let mut row = Vec::with_capacity(3); let mut row = Vec::with_capacity(3);
row.push( row.push(
widget::button::icon(widget::icon::from_name("go-previous-symbolic")) widget::button::icon(widget::icon::from_name("go-previous-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemLeft)) .on_press(Message::ItemLeft)
.into(), .into(),
); );
row.push( row.push(
widget::button::icon(widget::icon::from_name("go-next-symbolic")) widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemRight)) .on_press(Message::ItemRight)
.into(), .into(),
); );
if self.can_gallery() { if self.can_gallery() {
if let Some(_path) = self.path_opt() { if let Some(_path) = self.path_opt() {
row.push( row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
.on_press(app::Message::TabMessage(None, Message::Gallery(true))) .on_press(Message::Gallery(true))
.into(), .into(),
); );
} }
@ -1398,7 +1395,11 @@ impl Item {
row row
} }
pub fn preview_view<'a>(&'a self, sizes: IconSizes) -> Element<'a, app::Message> { pub fn preview_view<'a>(
&'a self,
mime_app_cache_opt: Option<&'a mime_app::MimeAppCache>,
sizes: IconSizes,
) -> Element<'a, Message> {
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
space_xxxs, space_xxxs,
space_m, space_m,
@ -1420,6 +1421,24 @@ impl Item {
mime = self.mime.to_string() mime = self.mime.to_string()
))); )));
let mut settings = Vec::new(); let mut settings = Vec::new();
if let Some(mime_app_cache) = mime_app_cache_opt {
let mime_apps = mime_app_cache.get(&self.mime);
if !mime_apps.is_empty() {
settings.push(
widget::settings::item::builder(fl!("open-with")).control(
widget::dropdown(
mime_apps,
mime_apps.iter().position(|x| x.is_default),
|index| {
let mime_app = &mime_apps[index];
Message::SetOpenWith(self.mime.clone(), mime_app.id.clone())
},
)
.icons(mime_app_cache.icons(&self.mime)),
),
);
}
}
match &self.metadata { match &self.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path { metadata, children } => {
if metadata.is_dir() { if metadata.is_dir() {
@ -1507,9 +1526,10 @@ impl Item {
column = column.push(details); column = column.push(details);
if let Some(path) = self.path_opt() { if let Some(path) = self.path_opt() {
column = column.push(widget::button::standard(fl!("open")).on_press( column = column.push(
app::Message::TabMessage(None, Message::Open(Some(path.to_path_buf()))), widget::button::standard(fl!("open"))
)); .on_press(Message::Open(Some(path.to_path_buf()))),
);
} }
if !settings.is_empty() { if !settings.is_empty() {
@ -1523,11 +1543,7 @@ impl Item {
column.into() column.into()
} }
pub fn replace_view<'a>( pub fn replace_view<'a>(&'a self, heading: String, sizes: IconSizes) -> Element<'a, Message> {
&'a self,
heading: String,
sizes: IconSizes,
) -> Element<'a, app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut row = widget::row().spacing(space_xxxs); let mut row = widget::row().spacing(space_xxxs);
@ -2243,8 +2259,8 @@ impl Tab {
if !item.selected { if !item.selected {
self.clicked = click_i_opt; self.clicked = click_i_opt;
item.selected = true; item.selected = true;
self.select_range = Some((i, i));
} }
self.select_range = Some((i, i));
self.select_focus = click_i_opt; self.select_focus = click_i_opt;
self.selected_clicked = true; self.selected_clicked = true;
} else if !dont_unset && item.selected { } else if !dont_unset && item.selected {
@ -2752,9 +2768,7 @@ impl Tab {
if let Some(items) = &mut self.items_opt { if let Some(items) = &mut self.items_opt {
if finished || context.ready.swap(false, atomic::Ordering::SeqCst) { if finished || context.ready.swap(false, atomic::Ordering::SeqCst) {
let duration = Instant::now(); let duration = Instant::now();
while let Some((path, name, metadata)) = while let Ok((path, name, metadata)) = context.results_rx.try_recv() {
context.results_rx.blocking_recv()
{
//TODO: combine this with column_sort logic, they must match! //TODO: combine this with column_sort logic, they must match!
let item_modified = metadata.modified().ok(); let item_modified = metadata.modified().ok();
let index = match items.binary_search_by(|other| { let index = match items.binary_search_by(|other| {
@ -2801,6 +2815,39 @@ impl Tab {
)); ));
} }
} }
Message::SelectFirst => {
if self.select_position(0, 0, mod_shift) {
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
}
Message::SelectLast => {
if let Some(ref items) = self.items_opt {
if let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max()
{
if self.select_position(last_pos.0, last_pos.1, mod_shift) {
if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset)
.into(),
));
}
if let Some(id) = self.select_focus_id() {
commands.push(Command::Iced(widget::button::focus(id).into()));
}
}
}
}
}
Message::SetOpenWith(mime, id) => {
commands.push(Command::SetOpenWith(mime, id));
}
Message::SetSort(heading_option, dir) => { Message::SetSort(heading_option, dir) => {
if !matches!(self.location, Location::Search(..)) { if !matches!(self.location, Location::Search(..)) {
self.sort_name = heading_option; self.sort_name = heading_option;
@ -2865,7 +2912,7 @@ impl Tab {
Message::Drop(Some((to, mut from))) => { Message::Drop(Some((to, mut from))) => {
self.dnd_hovered = None; self.dnd_hovered = None;
match to { match to {
Location::Path(to) => { Location::Desktop(to, ..) | Location::Path(to) => {
if let Ok(entries) = fs::read_dir(&to) { if let Ok(entries) = fs::read_dir(&to) {
for i in entries.into_iter().filter_map(|e| e.ok()) { for i in entries.into_iter().filter_map(|e| e.ok()) {
let i = i.path(); let i = i.path();
@ -3581,8 +3628,7 @@ impl Tab {
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> { pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
//TODO: left clicking on an empty folder does not clear context menu mouse_area::MouseArea::new(widget::column::with_children(vec![widget::container(
widget::column::with_children(vec![widget::container(
widget::column::with_children(match self.mode { widget::column::with_children(match self.mode {
Mode::App | Mode::Dialog(_) => vec![ Mode::App | Mode::Dialog(_) => vec![
widget::icon::from_name("folder-symbolic") widget::icon::from_name("folder-symbolic")
@ -3604,7 +3650,8 @@ impl Tab {
.spacing(space_xxs), .spacing(space_xxs),
) )
.center(Length::Fill) .center(Length::Fill)
.into()]) .into()]))
.on_press(|_| Message::Click(None))
.into() .into()
} }
@ -4285,9 +4332,9 @@ impl Tab {
.drag_content(move || { .drag_content(move || {
ClipboardCopy::new(crate::clipboard::ClipboardKind::Copy, &files) ClipboardCopy::new(crate::clipboard::ClipboardKind::Copy, &files)
}) })
.drag_icon(move || { .drag_icon(move |v| {
let state: tree::State = Widget::<Message, _, _>::state(&drag_list); let state: tree::State = Widget::<Message, _, _>::state(&drag_list);
(Element::from(drag_list.clone()).map(|_m| ()), state) (Element::from(drag_list.clone()).map(|_m| ()), state, v)
}) })
} }
_ => item_view, _ => item_view,
@ -4402,7 +4449,7 @@ impl Tab {
dnd_dest.into() dnd_dest.into()
} }
pub fn view<'a>(&'a self, key_binds: &'a HashMap<KeyBind, Action>) -> Element<Message> { pub fn view<'a>(&'a self, key_binds: &'a HashMap<KeyBind, Action>) -> Element<'a, Message> {
widget::responsive(|size| self.view_responsive(key_binds, size)).into() widget::responsive(|size| self.view_responsive(key_binds, size)).into()
} }

View file

@ -144,9 +144,7 @@ impl ThumbnailerCache {
} }
pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> { pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
self.cache self.cache.get(key).map_or_else(Vec::new, |x| x.clone())
.get(&key)
.map_or_else(|| Vec::new(), |x| x.clone())
} }
} }