Compare commits
24 commits
91243b99b5
...
9bcfe7a1f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bcfe7a1f5 | ||
|
|
b3af8bf211 | ||
|
|
4afacccc8a | ||
|
|
8c57060db2 | ||
|
|
62bfcc3550 | ||
|
|
9c0eb63b82 | ||
|
|
33890633b5 | ||
|
|
b895b07bb2 | ||
|
|
afca6aef73 | ||
|
|
9a89100088 | ||
|
|
908f30a571 | ||
|
|
1c4ab75814 | ||
|
|
e21989aaa3 | ||
|
|
15e40461e5 | ||
|
|
0bd20e57e7 | ||
|
|
e35d5123f0 | ||
|
|
971374f60b | ||
|
|
93dd775f3c | ||
|
|
bb15f30fe5 | ||
|
|
e2bdcf8da4 | ||
|
|
b299f1a172 | ||
|
|
c114759c9e | ||
|
|
fc25260a5f | ||
|
|
e60ae4e41f |
42 changed files with 1871 additions and 781 deletions
426
Cargo.lock
generated
426
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
26
Cargo.toml
26
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files"
|
||||
version = "1.0.9"
|
||||
version = "1.0.11"
|
||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
|
|
@ -14,7 +14,6 @@ icu = { version = "2.1.1", features = ["compiled_data"] }
|
|||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true }
|
||||
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11"
|
||||
gio = { version = "0.21", optional = true }
|
||||
glib = { version = "0.21", optional = true }
|
||||
glob = "0.3"
|
||||
|
|
@ -56,12 +55,16 @@ rust-embed = "8"
|
|||
slotmap = "1.1.1"
|
||||
recently-used-xbel = "1.2.0"
|
||||
zip = "8"
|
||||
uzers = "0.12.2"
|
||||
md-5 = "0.10.6"
|
||||
png = "0.18"
|
||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||
num_cpus = "1.17.0"
|
||||
filetime = "0.2"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
thiserror = "2.0.18"
|
||||
atomic_float = "1.1.0"
|
||||
num_enum = "0.7.6"
|
||||
|
||||
# Completion-based IO runtime to enable io_uring / IOCP file IO support.
|
||||
[dependencies.compio]
|
||||
|
|
@ -77,7 +80,6 @@ features = [
|
|||
"about",
|
||||
"advanced-shaping",
|
||||
"autosize",
|
||||
"desktop",
|
||||
"multi-window",
|
||||
"tokio",
|
||||
"winit",
|
||||
|
|
@ -109,7 +111,7 @@ default = [
|
|||
"wgpu",
|
||||
]
|
||||
dbus-config = ["libcosmic/dbus-config"]
|
||||
desktop = ["dep:cosmic-mime-apps", "dep:xdg"]
|
||||
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
||||
desktop-applet = []
|
||||
gvfs = ["dep:gio", "dep:glib"]
|
||||
io-uring = ["compio/io-uring"]
|
||||
|
|
@ -127,6 +129,7 @@ debug = true
|
|||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
fork = "0.7"
|
||||
uzers = "0.12.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
procfs = "0.18"
|
||||
|
|
@ -144,13 +147,12 @@ tokio = { version = "1", features = ["rt", "macros"] }
|
|||
# [patch.'https://github.com/pop-os/cosmic-text']
|
||||
# cosmic-text = { path = "../cosmic-text" }
|
||||
|
||||
# [patch.'https://github.com/pop-os/libcosmic']
|
||||
# libcosmic = { path = "../libcosmic" }
|
||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
[patch.'https://github.com/pop-os/libcosmic']
|
||||
libcosmic = { path = "../libcosmic" }
|
||||
cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||
cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||
iced_futures = { path = "../libcosmic/iced/futures" }
|
||||
iced_winit = { path = "../libcosmic/iced/winit" }
|
||||
|
||||
|
||||
# [patch.'https://github.com/pop-os/smithay-clipboard']
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files-applet"
|
||||
version = "1.0.9"
|
||||
version = "1.0.11"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
6
debian/changelog
vendored
6
debian/changelog
vendored
|
|
@ -1,3 +1,9 @@
|
|||
cosmic-files (1.0.11) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.11 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Tue, 14 Apr 2026 11:09:44 -0600
|
||||
|
||||
cosmic-files (1.0.9) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.9 version update
|
||||
|
|
|
|||
|
|
@ -9,9 +9,25 @@ use cosmic_files::dialog::{
|
|||
Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind,
|
||||
DialogMessage, DialogResult, DialogSettings,
|
||||
};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
let log_format = tracing_subscriber::fmt::format()
|
||||
.pretty()
|
||||
.without_time()
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_target(false)
|
||||
.with_thread_names(true);
|
||||
|
||||
let log_layer = tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(log_format);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
|
||||
.with(log_layer)
|
||||
.init();
|
||||
|
||||
let settings = Settings::default();
|
||||
app::run::<App>(settings, ())?;
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ empty-trash-warning = سيتم حذف العناصر الموجودة في مج
|
|||
|
||||
## New File/Folder Dialog
|
||||
|
||||
create-new-file = إنشاء ملف جديد
|
||||
create-new-folder = إنشاء مجلد جديد
|
||||
create-new-file = أنشئ ملف جديد
|
||||
create-new-folder = أنشئ مجلَّد جديد
|
||||
file-name = اسم الملف
|
||||
folder-name = اسم المجلد
|
||||
folder-name = اسم المجلَّد
|
||||
file-already-exists = يوجد ملف بهذا الاسم بالفعل
|
||||
folder-already-exists = يوجد مجلد بهذا الاسم بالفعل
|
||||
name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية
|
||||
|
|
@ -34,10 +34,10 @@ name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات م
|
|||
|
||||
## Open/Save Dialog
|
||||
|
||||
cancel = إلغاء
|
||||
open = فتح
|
||||
open-file = فتح ملف
|
||||
open-folder = افتح مجلد
|
||||
cancel = ألغِ
|
||||
open = افتح
|
||||
open-file = افتح ملف
|
||||
open-folder = افتح مجلَّد
|
||||
open-in-new-tab = افتح في لسان جديد
|
||||
open-in-new-window = افتح في نافذة جديدة
|
||||
open-multiple-files = افتح عدة ملفات
|
||||
|
|
@ -150,12 +150,12 @@ trashed-on = مهمل
|
|||
details = التفاصيل
|
||||
pause = ألبث
|
||||
resume = استئناف
|
||||
create-archive = إنشاء أرشيف
|
||||
create-archive = أنشئ أرشيف
|
||||
extract-password-required = كلمة السر مطلوبة
|
||||
extract-to = استخرِج إلى...
|
||||
extract-to-title = استخرِج إلى مجلّد
|
||||
mount-error = تعذر الوصول إلى القرص
|
||||
create = إنشاء
|
||||
create = أنشئ
|
||||
open-item-location = افتح مكان العنصر
|
||||
set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟
|
||||
favorite-path-error-description =
|
||||
|
|
@ -259,7 +259,7 @@ related-apps = تطبيقات ذات صلة
|
|||
selected-items = العناصر { $items } المحدّدة
|
||||
permanently-delete-question = احذف نهائيًا؟
|
||||
delete = احذف
|
||||
permanently-delete-warning = هل أنت متأكد من أنك تريد حذف { $target } نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.
|
||||
permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء.
|
||||
replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه.
|
||||
original-file = الملف الأصلي
|
||||
replace-with = استبدل بـ
|
||||
|
|
@ -371,3 +371,4 @@ move-to = انقل إلى...
|
|||
show-recents = مجلد الحديثة في الشريط الجانبي
|
||||
clear-recents-history = امحُ التأريخ الحديث
|
||||
copy-path = انسخ المسار
|
||||
mixed = مختلط
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ set-executable-and-launch-description = Chcete povolit spouštění souboru „{
|
|||
set-and-launch = Povolit a spustit
|
||||
open-with = Otevřít pomocí
|
||||
other = Ostatní
|
||||
none = Žádný
|
||||
none = Žádné
|
||||
icon-size-and-spacing = Velikost a rozestupy ikon
|
||||
grid-spacing = Rozestupy mřížky
|
||||
deleting =
|
||||
|
|
@ -417,3 +417,4 @@ move-to-title = Vyberte cíl přesunutí
|
|||
show-recents = Složka „Nedávné“ v postranním panelu
|
||||
copy-path = Kopírovat cestu
|
||||
clear-recents-history = Vymazat historii „Nedávné“
|
||||
mixed = Různé
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ light = Hell
|
|||
|
||||
type-to-search = Zum Suchen tippen
|
||||
type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner
|
||||
type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein
|
||||
type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein
|
||||
# Kontextmenü
|
||||
add-to-sidebar = Zur Seitenleiste hinzufügen
|
||||
compress = Komprimieren...
|
||||
|
|
@ -376,9 +376,9 @@ select-all = Alles auswählen
|
|||
|
||||
## Ansicht
|
||||
|
||||
zoom-in = Vergrößern
|
||||
zoom-in = Hineinzoomen
|
||||
default-size = Standardgröße
|
||||
zoom-out = Verkleinern
|
||||
zoom-out = Herauszoomen
|
||||
view = Ansicht
|
||||
grid-view = Rasteransicht
|
||||
list-view = Listenansicht
|
||||
|
|
@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien...
|
|||
|
||||
## Sortieren
|
||||
|
||||
sort = Sortieren
|
||||
sort = Sortierung
|
||||
sort-a-z = A-Z
|
||||
sort-z-a = Z-A
|
||||
sort-newest-first = Neueste zuerst
|
||||
|
|
@ -424,3 +424,8 @@ clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren
|
|||
comment = Dateimanager für den COSMIC Desktop
|
||||
keywords = Ordner;Manager;
|
||||
move-to-button-label = Verschieben
|
||||
move-to-title = Verschiebeziel auswählen
|
||||
remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen
|
||||
move-to = Verschieben nach...
|
||||
copy-path = Pfad kopieren
|
||||
mixed = Gemischt
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ no-results = No se encontraron resultados
|
|||
filesystem = Sistema de archivos
|
||||
home = Inicio
|
||||
networks = Redes
|
||||
notification-in-progress = Las operaciones de archivo están en progreso.
|
||||
notification-in-progress = Las operaciones de archivo están en progreso
|
||||
trash = Papelera
|
||||
recents = Recientes
|
||||
undo = Deshacer
|
||||
|
|
|
|||
0
i18n/eu/cosmic_files.ftl
Normal file
0
i18n/eu/cosmic_files.ftl
Normal file
|
|
@ -5,7 +5,7 @@ no-results = Ei tuloksia
|
|||
filesystem = Tiedostojärjestelmä
|
||||
home = Koti
|
||||
networks = Verkot
|
||||
notification-in-progress = Tiedostotoimintoja käynnissä.
|
||||
notification-in-progress = Tiedostotoimintoja käynnissä
|
||||
trash = Roskakori
|
||||
recents = Viimeaikaiset
|
||||
undo = Kumoa
|
||||
|
|
@ -16,7 +16,7 @@ today = Tänään
|
|||
desktop-view-options = Työpöytänäkymän asetukset…
|
||||
show-on-desktop = Näytä työpöydällä
|
||||
desktop-folder-content = Työpöytäkansion sisältö
|
||||
mounted-drives = Tiedostojärjestelmään liitetyt kovalevyt
|
||||
mounted-drives = Liitetyt asemat
|
||||
trash-folder-icon = Roskakorikansion kuvake
|
||||
icon-size-and-spacing = Kuvakkeen koko ja välistys
|
||||
icon-size = Kuvakkeen koko
|
||||
|
|
@ -38,7 +38,7 @@ create-archive = Luo arkisto
|
|||
## Empty Trash Dialog
|
||||
|
||||
empty-trash = Tyhjennä roskakori
|
||||
empty-trash-warning = Haluatko varmasti tyhjentää koko roskakorin pysyvästi?
|
||||
empty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi
|
||||
|
||||
## Mount Error Dialog
|
||||
|
||||
|
|
@ -73,8 +73,8 @@ save-file = Tallenna tiedosto
|
|||
|
||||
## Open With Dialog
|
||||
|
||||
open-with-title = Kuinka haluat avata kohteen "{ $name }"?
|
||||
browse-store = Selaa { $store }
|
||||
open-with-title = Miten haluat avata kohteen "{ $name }"?
|
||||
browse-store = Selaa { $store }a
|
||||
|
||||
## Rename Dialog
|
||||
|
||||
|
|
@ -88,15 +88,15 @@ replace-title = "{ $filename }" on jo olemassa tässä sijainnissa
|
|||
replace-warning = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön.
|
||||
replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön.
|
||||
original-file = Alkuperäinen tiedosto
|
||||
replace-with = Korvaa kohteella
|
||||
apply-to-all = Sovella kaikkiin
|
||||
replace-with = Korvaa käyttäen
|
||||
apply-to-all = Toteuta kaikkiin
|
||||
keep-both = Pidä molemmat
|
||||
skip = Ohita
|
||||
|
||||
## Set as Executable and Launch Dialog
|
||||
|
||||
set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä
|
||||
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettväksi ja käynnistää sen?
|
||||
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettäväksi ja käynnistää sen?
|
||||
set-and-launch = Aseta ja käynnistä
|
||||
|
||||
## Metadata Dialog
|
||||
|
|
@ -116,9 +116,9 @@ other = Muut
|
|||
add-network-drive = Lisää verkkolevy
|
||||
connect = Yhdistä
|
||||
connect-anonymously = Yhdistä nimettömästi
|
||||
connecting = Yhdistää…
|
||||
connecting = Yhdistetään…
|
||||
domain = Verkkotunnus
|
||||
enter-server-address = Syötä palvelimen osoite
|
||||
enter-server-address = Kirjoita palvelimen osoite
|
||||
network-drive-description =
|
||||
Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen.
|
||||
Esimerkkejä: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
|
|
@ -126,91 +126,91 @@ network-drive-description =
|
|||
### Make sure to keep the comma which separates the columns
|
||||
|
||||
network-drive-schemes =
|
||||
Saatavissa olevat protokollat,Etuliite
|
||||
Saatavilla olevat yhteyskäytännöt,Etuliite
|
||||
AppleTalk,afp://
|
||||
File Transfer Protocol,ftp:// or ftps://
|
||||
File Transfer Protocol,ftp:// tai ftps://
|
||||
Network File System,nfs://
|
||||
Server Message Block,smb://
|
||||
SSH File Transfer Protocol,sftp:// or ssh://
|
||||
WebDav,dav:// or davs://
|
||||
network-drive-error = Verkkolevy saavuttamattomissa
|
||||
SSH File Transfer Protocol,sftp:// tai ssh://
|
||||
WebDav,dav:// tai davs://
|
||||
network-drive-error = Verkkolevy ei saatavilla
|
||||
password = Salasana
|
||||
remember-password = Muista salasana
|
||||
try-again = Yritä uudelleen
|
||||
username = Käyttäjänimi
|
||||
username = Käyttäjätunnus
|
||||
|
||||
## Operations
|
||||
|
||||
edit-history = Muokkaa historiaa
|
||||
history = Historia
|
||||
no-history = Historia on tyhjä.
|
||||
pending = Odottaa käsittelyä
|
||||
failed = Epäonnistui
|
||||
complete = Valmis
|
||||
pending = Jonossa
|
||||
failed = Epäonnistuneet
|
||||
complete = Valmiit
|
||||
compressing =
|
||||
Tiivistetään { $items } { $items ->
|
||||
Pakataan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteita
|
||||
} lähteestä "{ $from }" arkistoon "{ $to }"
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })…
|
||||
compressed =
|
||||
Tiivistetty { $items } { $items ->
|
||||
Pakattu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} lähteestä "{ $from }" arkistoon "{ $to }"
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" arkistoon "{ $to }"
|
||||
copy_noun = Kopio
|
||||
creating = Luodaan kohdetta "{ $name }" kohteen "{ $parent }" alle
|
||||
created = Luotu kohde "{ $name }" kohteen "{ $parent }" alle
|
||||
creating = Luodaan "{ $name }" kohteen "{ $parent }" alle
|
||||
created = Luotu "{ $name }" kohteen "{ $parent }" alle
|
||||
copying =
|
||||
Kopioidaan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteita
|
||||
} lähteestä "{ $from }" kohteeseen "{ $to }"
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
|
||||
copied =
|
||||
Kopioitu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} lähteestä "{ $from }" kohteeseen "{ $to }"
|
||||
emptying-trash = Tyhjennetään { trash }
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" kohteeseen "{ $to }"
|
||||
emptying-trash = Tyhjennetään { trash } ({ $progress })…
|
||||
emptied-trash = Tyhjennetty { trash }
|
||||
extracting =
|
||||
Puretaan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} arkistosta "{ $from }" kohteeseen "{ $to }"
|
||||
*[other] kohdetta
|
||||
} arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })…
|
||||
extracted =
|
||||
Purettu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
*[other] kohdetta
|
||||
} arkistosta "{ $from }" kohteeseen "{ $to }"
|
||||
setting-executable-and-launching = Asetetaan "{ $name }" käynnistettäväksi ja käynnistetään
|
||||
set-executable-and-launched = Asetettu "{ $name }" käynnistettäväksi ja käynnistetty
|
||||
moving =
|
||||
Siirretään { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} lähteestä "{ $from }" kohteeseen "{ $to }"
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })…
|
||||
moved =
|
||||
Siirretty { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} lähteestä "{ $from }" kohteeseen "{ $to }"
|
||||
*[other] kohdetta
|
||||
} sijainnista "{ $from }" kohteeseen "{ $to }"
|
||||
renaming = Nimetään kohde "{ $from }" muotoon "{ $to }"
|
||||
renamed = Nimetty kohde "{ $from }" muotoon "{ $to }"
|
||||
restoring =
|
||||
Palautetaan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} { trash }sta
|
||||
*[other] kohdetta
|
||||
} roskakorista ({ $progress })…
|
||||
restored =
|
||||
Palautettu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohteet
|
||||
} { trash }sta
|
||||
unknown-folder = Tuntematon kansio
|
||||
*[other] kohdetta
|
||||
} roskakorista
|
||||
unknown-folder = tuntematon kansio
|
||||
|
||||
## Open with
|
||||
|
||||
menu-open-with = Avaa ohjelmalla…
|
||||
menu-open-with = Avaa sovelluksella…
|
||||
default-app = { $name } (oletus)
|
||||
|
||||
## Show details
|
||||
|
|
@ -225,14 +225,14 @@ settings = Asetukset
|
|||
|
||||
appearance = Ulkoasu
|
||||
theme = Teema
|
||||
match-desktop = Sovita yhteen työpöydän kanssa
|
||||
match-desktop = Sovita työpöytään
|
||||
dark = Tumma
|
||||
light = Vaalea
|
||||
|
||||
# Context menu
|
||||
|
||||
add-to-sidebar = Lisää sivupalkkiin
|
||||
compress = Pakkaa
|
||||
compress = Pakkaa…
|
||||
extract-here = Pura
|
||||
new-file = Uusi tiedosto…
|
||||
new-folder = Uusi kansio…
|
||||
|
|
@ -241,9 +241,9 @@ move-to-trash = Siirrä roskakoriin
|
|||
restore-from-trash = Palauta roskakorista
|
||||
remove-from-sidebar = Poista sivupalkista
|
||||
sort-by-name = Järjestä nimen mukaan
|
||||
sort-by-modified = Järjestä muokkauspäivämäärän mukaan
|
||||
sort-by-modified = Järjestä muokkausajan mukaan
|
||||
sort-by-size = Järjestä koon mukaan
|
||||
sort-by-trashed = Järjestä poistamispäivämäärän mukaan
|
||||
sort-by-trashed = Järjestä poistamisajan mukaan
|
||||
|
||||
## Desktop
|
||||
|
||||
|
|
@ -281,9 +281,9 @@ grid-view = Ruudukkonäkymä
|
|||
list-view = Listanäkymä
|
||||
show-hidden-files = Näytä piilotetut tiedostot
|
||||
list-directories-first = Näytä kansiot ensin
|
||||
gallery-preview = Gallerian esinäkymä
|
||||
gallery-preview = Gallerian esikatselu
|
||||
menu-settings = Asetukset…
|
||||
menu-about = Tietoa COSMIC Tiedostoista…
|
||||
menu-about = Tietoa COSMICin tiedostonhallinnasta…
|
||||
|
||||
## Sort
|
||||
|
||||
|
|
@ -318,3 +318,90 @@ move-to-button-label = Siirrä
|
|||
clear-recents-history = Tyhjennä viimeaikaisten historia
|
||||
copy-path = Kopioi polku
|
||||
dismiss = Hylkää viesti
|
||||
operations-running =
|
||||
{ $running } { $running ->
|
||||
[one] toiminto
|
||||
*[other] toimintoa
|
||||
} käynnissä ({ $percent } %)...
|
||||
operations-running-finished =
|
||||
{ $running } { $running ->
|
||||
[one] toiminto
|
||||
*[other] toimintoa
|
||||
} käynnissä ({ $percent } %), { $finished } valmistunut…
|
||||
pause = Keskeytä
|
||||
extract-to = Pura sijaintiin…
|
||||
permanently-delete-warning = { $target } tullaan poistamaan pysyvästi. Tätä toimintoa ei voi perua.
|
||||
execute-only = Vain suoritus
|
||||
write-only = Vain kirjoitus
|
||||
write-execute = Kirjoita ja suorita
|
||||
read-only = Vain luku
|
||||
read-execute = Lue ja suorita
|
||||
read-write = Lue ja kirjoita
|
||||
read-write-execute = Lue, kirjoita sekä suorita
|
||||
calculating = Lasketaan…
|
||||
single-click = Yhden napsautuksen avaus
|
||||
type-to-search = Kirjoita etsiäksesi
|
||||
type-to-search-recursive = Etsii nykyisestä kansiosta ja kaikista alikansioista
|
||||
remove-from-recents = Poista viimeaikaisista
|
||||
selected-items = { $items } valittua kohdetta
|
||||
show-recents = Viimeaikaisten kansio sivupalkissa
|
||||
copy-to = Kopioi…
|
||||
move-to = Siirrä…
|
||||
details = Yksityiskohdat
|
||||
grid-spacing = Ruudukkovälit
|
||||
none = Ei mitään
|
||||
favorite-path-error = Virhe avattaessa kansiota
|
||||
favorite-path-error-description =
|
||||
Polun { $path } avaaminen ei onnistunut
|
||||
"{ $path }" ei välttämättä ole olemassa tai oikeutesi eivät riitä sen avaamiseen
|
||||
|
||||
Haluatko poistaa sen sivupalkista?
|
||||
keep = Pidä
|
||||
repository = Tietovarasto
|
||||
support = Tuki
|
||||
progress = { $percent } %
|
||||
progress-cancelled = { $percent } %, peruttu
|
||||
progress-failed = { $percent } %, epäonnistui
|
||||
progress-paused = { $percent } %, keskeytetty
|
||||
setting-permissions = Asetetaan kohteen "{ $name }" käyttöoikeudeksi { $mode }
|
||||
set-permissions = Asetettu kohteen { $name } käyttöoikeudeksi { $mode }
|
||||
permanently-deleting =
|
||||
Poistetaan pysyvästi { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
}
|
||||
permanently-deleted =
|
||||
Poistettu pysyvästi { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
}
|
||||
items = Kohteita: { $items }
|
||||
item-accessed = Käytetty: { $accessed }
|
||||
type-to-search-enter-path = Kirjoittaa polun kansioon tai tiedostoon
|
||||
eject = Poista asemasta
|
||||
copy-to-title = Valitse mihin kopioidaan
|
||||
move-to-title = Valitse mihin siirretään
|
||||
pasted-image = Liitetty kuva
|
||||
pasted-text = Liitetty teksti
|
||||
pasted-video = Liitetty video
|
||||
type-to-search-select = Valitsee ensimmäisen täsmäävän tiedoston tai kansion
|
||||
deleting =
|
||||
Poistetaan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
} roskakorista ({ $progress })…
|
||||
deleted =
|
||||
Poistettu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
} roskakorista
|
||||
removing-from-recents =
|
||||
Poistetaan { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
} viimeaikaisista
|
||||
removed-from-recents =
|
||||
Poistettu { $items } { $items ->
|
||||
[one] kohde
|
||||
*[other] kohdetta
|
||||
} viimeaikaisista
|
||||
|
|
|
|||
|
|
@ -436,3 +436,4 @@ keywords = Dossier;Gestionnaire;
|
|||
show-recents = Dossier Récents dans la barre latérale
|
||||
copy-path = Copier le chemin
|
||||
clear-recents-history = Effacer l'historique des Récents
|
||||
mixed = Mixte
|
||||
|
|
|
|||
|
|
@ -433,3 +433,4 @@ keywords = Fillteán;Bainisteoir;
|
|||
show-recents = Fillteán le déanaí sa bharra taoibh
|
||||
clear-recents-history = Glan stair na n-earraí le déanaí
|
||||
copy-path = Cóipeáil an chosán
|
||||
mixed = Measctha
|
||||
|
|
|
|||
|
|
@ -436,3 +436,4 @@ move-to = Áthelyezés ide…
|
|||
show-recents = Legutóbbiak mappa megjelenítése az oldalsávban
|
||||
copy-path = Útvonal másolása
|
||||
clear-recents-history = Legutóbbiak előzményének törlése
|
||||
mixed = Vegyes
|
||||
|
|
|
|||
|
|
@ -318,3 +318,4 @@ comment = Pengelola berkas untuk desktop COSMIC
|
|||
show-recents = Map terbaru di bilah sisi
|
||||
clear-recents-history = Bersihkan riwayat Terbaru
|
||||
copy-path = Salin jalur
|
||||
mixed = Bercampur
|
||||
|
|
|
|||
|
|
@ -1 +1,322 @@
|
|||
change-wallpaper = Beddel aɣrab n ugdil…
|
||||
cosmic-files = Ifuyla COSMIC
|
||||
empty-folder = Akaram d ilem
|
||||
empty-folder-hidden = Akaram d ilem (yesɛa iferdisen yeffren)
|
||||
no-results = Ulac igmaḍ yettwafen
|
||||
home = Agejdan
|
||||
networks = Iẓeḍwa
|
||||
notification-in-progress = Timhalin ɣef ifuyla la tteddunt
|
||||
trash = Iḍumman
|
||||
recents = Melmi kan
|
||||
undo = Ssemmet
|
||||
today = Ass-a
|
||||
desktop-view-options = Iɣewwaṛen n tmeẓri n tnarit…
|
||||
show-on-desktop = Sken deg tnarit
|
||||
desktop-folder-content = Agbur n ukaram n tnarit
|
||||
trash-folder-icon = Tignit n ukaram n iḍumman
|
||||
icon-size-and-spacing = Tiddi n tignit akked tallunt
|
||||
icon-size = Tiddi n tignit
|
||||
name = Isem
|
||||
modified = Ittusnifel
|
||||
trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman
|
||||
size = Tiddi
|
||||
details = Talqayt
|
||||
pause = Serǧu
|
||||
resume = Kemmel
|
||||
create-archive = Snulfu-d aɣbaṛ
|
||||
extract-to = Ssef ɣer...
|
||||
extract-to-title = Ssef ɣer ukaram
|
||||
empty-trash = Silem iḍumman
|
||||
rename-folder = Snifel isem n ukaram
|
||||
filesystem = Anagraw n yifuyla
|
||||
dismiss = Zgel izen
|
||||
empty-trash-title = Silem iḍumman?
|
||||
empty-trash-warning = Iferdisen n ukaram n iḍumman ad ttwakksen i lebda
|
||||
create-new-file = Snulfu-d afaylu amaynut
|
||||
create-new-folder = Snulfu-d akaram amaynut
|
||||
file-name = Isem n ufaylu
|
||||
folder-name = Isem n ukaram
|
||||
file-already-exists = Afaylu s yisem-agi yella yakan
|
||||
folder-already-exists = Akaram s yisem-agi yella yakan
|
||||
name-hidden = Ismawen ibeddun s "." ad ttwaffren
|
||||
name-invalid = Isem ur yezmir ara ad yili "{ $filename }"
|
||||
cancel = Sefsex
|
||||
create = Snulfu-d
|
||||
open = Ldi
|
||||
open-file = Ldi afaylu
|
||||
open-folder = Ldi akaram
|
||||
open-in-new-tab = Ldi deg yiccer amaynut
|
||||
open-in-new-window = Ldi deg usfaylu amaynut
|
||||
open-item-location = Ldi adig n uferdis
|
||||
open-multiple-files = Ldi aget n ifuyla
|
||||
mounted-drives = imeɣriyen yettuserkben
|
||||
mount-error = Ulamek anekcum ɣer umeɣri
|
||||
operations-running =
|
||||
{ $running } { $running ->
|
||||
[one] n temhelt la tteddu
|
||||
*[other] n temhal la tteddunt
|
||||
} ({ $percent }%)...
|
||||
operations-running-finished =
|
||||
{ $running } { $running ->
|
||||
[one] n temhelt la tteddu
|
||||
*[other] n temhal la tteddunt
|
||||
} ({ $percent }%), { $finished } { $finished ->
|
||||
[one] tfukk
|
||||
*[other] fukkent
|
||||
}...
|
||||
copy-to-title = Fren taɣerwaḍt n unɣel
|
||||
copy-to-button-label = Nɣel
|
||||
move-to-title = Fren taɣerwaḍt n usmutti
|
||||
move-to-button-label = Smutti
|
||||
comment = Amsefrak n yifuyla i tnarit COSMIC
|
||||
keywords = Akaram;Amsefrak;
|
||||
delete = kkes
|
||||
replace = Semselsi
|
||||
support = Tallalt
|
||||
settings = Iɣewwaṛen
|
||||
appearance = Timeẓri
|
||||
theme = Asentel
|
||||
dark = Aɣmayan
|
||||
light = Aceɛlal
|
||||
paste = Senteḍ
|
||||
select-all = Fren akk
|
||||
zoom-in = Asemɣeṛ
|
||||
default-size = Tiddi tamezwert
|
||||
zoom-out = Asemẓi
|
||||
view = Wali
|
||||
menu-settings = Iɣewwaṛen…
|
||||
save = Sekles
|
||||
match-desktop = Amṣada d tnarit
|
||||
file = Afaylu
|
||||
new-window = Asfaylu amaynut
|
||||
quit = Tuffɣa
|
||||
edit = Ẓreg
|
||||
cut = Gzem
|
||||
copy = Nɣel
|
||||
repository = Asarsay
|
||||
pasted-image = Tettwasenṭeḍ tugna
|
||||
pasted-text = Yettwasenṭeḍ uḍris
|
||||
pasted-video = Tettwasenṭeḍ tvidyut
|
||||
compress = Skussem…
|
||||
grid-spacing = Tallunt n iẓiki
|
||||
extract-password-required = Awal uffir yettwasra
|
||||
permanently-delete-question = Kkes s wudem imezgi?
|
||||
permanently-delete-warning = { $target } ad yettwakkes s wudem imezgi. Tigawt-agi ur tezmir ara ad tettwasefsex.
|
||||
rename-file = Snifel isem n ufaylu
|
||||
replace-title = "{ $filename }" yella yakan deg wadig-a
|
||||
replace-warning = Tebɣiḍ ad t-tsemselsiḍ s win ara teskelseḍ? Asemselsi-ines ad yaru sennig ugbur-is.
|
||||
replace-warning-operation = Tebɣiḍ ad t-tsemselsiḍ? Asemselsi-ines ad yaru sennig ugbur-is.
|
||||
original-file = Afaylu aneṣli
|
||||
replace-with = Semselsi s
|
||||
apply-to-all = Snes i meṛṛa
|
||||
keep-both = Eǧǧ-iten deg sin
|
||||
skip = Zgel
|
||||
set-executable-and-launch = Sbeddet am umselkam syinna senker
|
||||
set-executable-and-launch-description = Tebɣiḍ ad tesbeddeḍ "{ $name }" am umselkam syinna ad tessenkreḍ?
|
||||
set-and-launch = Sbadu sakin senker
|
||||
add-network-drive = Rnu ameɣri n uẓeṭṭa
|
||||
connect = Qqen
|
||||
connect-anonymously = Qqen s wudem udrig
|
||||
connecting = Tuqqna…
|
||||
domain = Taɣult
|
||||
enter-server-address = Sekcem tansa n uqeddac
|
||||
network-drive-description =
|
||||
Tansiwin n uqeddac gebrent azwir n uneggaf akked tansa.
|
||||
Imedyaten: ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
network-drive-error = Ur izmir ara ad yekcem ɣer umeɣri n uzeṭṭa
|
||||
password = Awal uffir
|
||||
remember-password = Cfu ɣef wawal uffir
|
||||
try-again = ɛreḍ tikelt nniḍen
|
||||
username = Isem n useqdac
|
||||
cancelled = Yettwasefsex
|
||||
edit-history = Ẓreg amazray
|
||||
history = Amazray
|
||||
no-history = Ulac iferdisen deg umazray.
|
||||
pending = Yettṛaǧu
|
||||
progress-cancelled = { $percent }%, yettwasefsex
|
||||
progress-failed = { $percent }%, ur yeddi ara
|
||||
failed = Ur yeddi ara
|
||||
renaming = Asnifel n yisem "{ $from }" ɣer "{ $to }"
|
||||
renamed = Yettwasenfel yisem n "{ $from }" ɣer "{ $to }"
|
||||
unknown-folder = akaram arussin
|
||||
menu-open-with = Ldi s…
|
||||
default-app = { $name } (amezwer)
|
||||
show-details = Sken talqayt
|
||||
type = Anaw: { $mime }
|
||||
items = Iferdisen: { $items }
|
||||
item-size = Tiddi: { $size }
|
||||
item-created = Yettwarna: { $created }
|
||||
item-modified = Ittusnifel: { $modified }
|
||||
item-accessed = Yettwakcem: { $accessed }
|
||||
calculating = Asiḍen…
|
||||
single-click = Asiti asuf i ulday
|
||||
type-to-search = Aru iwakken ad tnadiḍ
|
||||
type-to-search-recursive = Nadi akaram amiran akked ikaramen inaddawen meṛṛa
|
||||
type-to-search-enter-path = Sekcem abrid ɣer ukaram neɣ afaylu
|
||||
add-to-sidebar = Rnu ɣer ufeggag adisan
|
||||
delete-permanently = Kkes i lebda
|
||||
grid-view = Askan s iẓiki
|
||||
list-view = Askan s tebdart
|
||||
show-hidden-files = Sken ifuyla uffiren
|
||||
gallery-preview = Taskant n temidelt
|
||||
menu-about = Ɣef Ifuyla COSMIC…
|
||||
sort = Asmizzwer
|
||||
sort-newest-first = Amaynut d amezwaru
|
||||
sort-oldest-first = Aqbur d amezwaru
|
||||
sort-smallest-to-largest = Seg umeẓyan akk ɣer umeqqran
|
||||
sort-largest-to-smallest = Seg umeqqran akk ɣer umeẓyan
|
||||
open-multiple-folders = Ldi usgit n ikaramen
|
||||
save-file = Sekles afaylu
|
||||
open-with-title = Amek tebɣiḍ ad teldiḍ "{ $name }"?
|
||||
browse-store = Snirem { $store }
|
||||
other-apps = Isnasen-nniḍen
|
||||
emptying-trash = Silem { trash } ({ $progress })…
|
||||
emptied-trash = D ilem { trash }
|
||||
set-permissions = Sbadu isirigen i "{ $name }" ɣer { $mode }
|
||||
eject = Ḍeqqer
|
||||
extract-here = Ssef
|
||||
new-file = Afaylu amaynut…
|
||||
new-folder = Akaram amaynut…
|
||||
open-in-terminal = Ldi deg yixef
|
||||
move-to-trash = Smutti ɣer tqecwalt n yiḍumman
|
||||
restore-from-trash = Err-d seg tqecwalt n yiḍumman
|
||||
remove-from-sidebar = Kkes seg ugalis adisan
|
||||
sort-by-name = Smizzwer s yisem
|
||||
sort-by-modified = Asmizzwer s usnifel
|
||||
sort-by-size = Asmizzwer s tiddi
|
||||
sort-by-trashed = Asmizzwer s wakud n tukksa
|
||||
remove-from-recents = Kkes seg ineggura
|
||||
desktop-appearance = Timeẓri n tnarit…
|
||||
display-settings = Iɣewwaṛen n ubeqqeḍ...
|
||||
new-tab = Iccer amaynut
|
||||
reload-folder = Ales asali n ukaram
|
||||
rename = Snifel isem...
|
||||
close-tab = Mdel iccer
|
||||
list-directories-first = Sken di tazwara ikaramen
|
||||
open-with = Ldi s
|
||||
owner = Bab
|
||||
group = Agraw
|
||||
other = Ayen nniḍen
|
||||
none = Ula Yiwen
|
||||
execute-only = Selkem kan
|
||||
write-only = Aru kan
|
||||
write-execute = Aru u selkem
|
||||
read-only = I tɣuri kan
|
||||
read-execute = Ɣeṛ u selkem
|
||||
read-write = Ɣeṛ u aru
|
||||
read-write-execute = Ɣeṛ, aru, u selkem
|
||||
favorite-path-error = Tuccḍa deg ulday n ukaram
|
||||
remove = Kkes
|
||||
keep = Eǧǧ
|
||||
progress = { $percent }%
|
||||
progress-paused = { $percent }%, ibedd
|
||||
complete = Immed
|
||||
copy_noun = Nɣel
|
||||
creating = Asnulfu n "{ $name }" deg "{ $parent }"
|
||||
created = Yettwarna "{ $name }" deg "{ $parent }"
|
||||
setting-executable-and-launching = Asbadu n "{ $name }" am umselkam syin ad yettwasekker
|
||||
set-executable-and-launched = Sbadu "{ $name }" am umselkam syin sekker-it
|
||||
setting-permissions = Asbadu n tsirag i "{ $name }" ɣer { $mode }
|
||||
related-apps = Isnasen icudden
|
||||
favorite-path-error-description =
|
||||
Ur izmir ara ad yeldi "{ $path }"
|
||||
"{ $path }" yezmer lḥal ulac-it neɣ ahat ur tesɛiḍ ara tisirag akken ad t-teldiḍ
|
||||
|
||||
Tebɣiḍ ad t-tekkseḍ seg ufeggag adisan?
|
||||
compressing =
|
||||
La issekkussum { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
|
||||
compressed =
|
||||
Yekussem { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }"
|
||||
copying =
|
||||
Anɣal { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
|
||||
copied =
|
||||
Yettwanɣel { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }"
|
||||
deleting =
|
||||
Tukksa { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { trash } ({ $progress })...
|
||||
deleted =
|
||||
Yettwakkes { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { trash }
|
||||
extracting =
|
||||
Tussfa { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
|
||||
extracted =
|
||||
Yettusef { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }"
|
||||
moving =
|
||||
Asmutti { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }" ({ $progress })...
|
||||
moved =
|
||||
Yettwasenkez { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg "{ $from }" ɣer "{ $to }"
|
||||
permanently-deleting =
|
||||
Tukksa i lebda { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
}
|
||||
permanently-deleted =
|
||||
I lebda yettwakkes { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
}
|
||||
removing-from-recents =
|
||||
Tukksa { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { recents }
|
||||
removed-from-recents =
|
||||
Yettwakkes { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { recents }
|
||||
restoring =
|
||||
Tiririt { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { trash } ({ $progress })…
|
||||
restored =
|
||||
Yettwarr-d { $items } { $items ->
|
||||
[one] n uferdis
|
||||
*[other] n yiferdisen
|
||||
} seg { trash }
|
||||
network-drive-schemes =
|
||||
Ineggafen iwejden, azwir
|
||||
AppleTalk,afp://
|
||||
Aneggaf n usiweḍ n yifuyla,ftp:// neɣ ftps://
|
||||
Anagraw n yifuyla n uzeṭṭa,nfs://
|
||||
Iḥder n yizen n uqeddac,smb://
|
||||
SSH Aneggaf n usiweḍ n yifuyla,sftp:// neɣ ssh://
|
||||
WebDAV:// neɣ davs://
|
||||
sort-a-z = A-Ẓ
|
||||
sort-z-a = Ẓ-A
|
||||
selected-items = { $items } n yiferdisen yettwafernen
|
||||
type-to-search-select = Fren afaylu amezwaru neɣ akaram yemṣadan
|
||||
copy-to = Nɣel ɣer...
|
||||
move-to = Smutti ɣer…
|
||||
show-recents = Akaram n melmi kan deg ufeggag adisan
|
||||
clear-recents-history = Sfeḍ azray n melmi kan
|
||||
copy-path = Nɣel abrid
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ type-to-search-recursive = Ағымдағы бума мен барлық ішк
|
|||
type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді
|
||||
type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды
|
||||
add-to-sidebar = Бүйірлік панельге қосу
|
||||
compress = Сығу
|
||||
compress = Сығу...
|
||||
delete-permanently = Біржолата өшіру
|
||||
eject = Шығару
|
||||
extract-here = Тарқату
|
||||
|
|
@ -318,3 +318,4 @@ keywords = Folder;Manager;Бума;Басқарушы;
|
|||
show-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы
|
||||
clear-recents-history = Жуырдағылар тарихын өшіру
|
||||
copy-path = Орналасқан жолын көшіру
|
||||
mixed = Аралас
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ dismiss = 메시지 무시
|
|||
copy_noun = 복사
|
||||
progress = { $percent }%
|
||||
related-apps = 관련 앱
|
||||
compress = 압축
|
||||
compress = 압축...
|
||||
network-drive-error = 네트워크 드라이브에 접근할 수 없음
|
||||
icon-size-and-spacing = 아이콘 크기 및 간격
|
||||
password = 암호
|
||||
|
|
@ -272,3 +272,8 @@ network-drive-schemes =
|
|||
WebDAV,dav:// 또는 davs://
|
||||
type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다
|
||||
comment = COSMIC 데스크톱용 위한 파일 관리자
|
||||
keywords = 폴더;관리자;
|
||||
copy-to-button-label = 복사
|
||||
move-to-button-label = 이동
|
||||
clear-recents-history = 최근 기록 비우기
|
||||
copy-path = 복사 경로
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ type-to-search = Norint ieškoti, pradėkite rašyti
|
|||
type-to-search-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose
|
||||
type-to-search-enter-path = Įvedamas aplanko ar failo kelias
|
||||
add-to-sidebar = Pridėti į šonjuostę
|
||||
compress = Suspausti
|
||||
compress = Suspausti...
|
||||
delete-permanently = Ištrinti visam laikui
|
||||
eject = Išstumti
|
||||
extract-here = Išskleisti
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pause = ਵਿਰਾਮ
|
|||
resume = ਮੁੜ-ਚਾਲੂ
|
||||
create-archive = ਅਕਾਇਵ ਬਣਾਓ
|
||||
extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ
|
||||
extract-to = Extract To...
|
||||
extract-to = ਖਿਲਾਰੋ...
|
||||
extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ
|
||||
empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ
|
||||
empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ?
|
||||
|
|
@ -113,7 +113,7 @@ replace-title = "{ $filename }" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣ
|
|||
favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ
|
||||
add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ
|
||||
connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ
|
||||
connecting = Connecting...
|
||||
connecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...
|
||||
domain = ਡੋਮੇਨ
|
||||
enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ
|
||||
password = ਪਾਸਵਰਡ
|
||||
|
|
@ -138,9 +138,9 @@ sort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ
|
|||
sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ
|
||||
sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ
|
||||
remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ
|
||||
change-wallpaper = Change wallpaper...
|
||||
desktop-appearance = Desktop appearance...
|
||||
display-settings = Display settings...
|
||||
change-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ...
|
||||
desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ...
|
||||
display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ...
|
||||
file = ਫ਼ਾਇਲ
|
||||
new-tab = ਨਵੀਂ ਟੈਬ
|
||||
new-window = ਨਵੀਂ ਵਿੰਡੋ
|
||||
|
|
@ -195,11 +195,11 @@ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ
|
|||
type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ
|
||||
add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ
|
||||
compress = ਕੰਪਰੈਸ
|
||||
copy-to = Copy to...
|
||||
copy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ...
|
||||
delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ
|
||||
eject = ਬਾਹਰ
|
||||
extract-here = ਖਿਲਾਰੋ
|
||||
move-to = Move to...
|
||||
move-to = ਇੱਥੇ ਭੇਜੋ...
|
||||
remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ
|
||||
reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ
|
||||
list-view = ਸੂਚੀ ਝਲਕ
|
||||
|
|
|
|||
|
|
@ -437,3 +437,4 @@ move-to = Przenieś do…
|
|||
show-recents = Ostatnie katalogi w panelu bocznym
|
||||
clear-recents-history = Wyczyść bierzącą historię
|
||||
copy-path = Skopiuj ścieżkę
|
||||
mixed = Mieszane
|
||||
|
|
|
|||
|
|
@ -424,8 +424,8 @@ sort-largest-to-smallest = Do maior para o menor
|
|||
empty-trash-title = Esvaziar a lixeira?
|
||||
type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente
|
||||
pasted-image = Imagem colada
|
||||
pasted-text = Texto copiado
|
||||
pasted-video = Vídeo copiado
|
||||
pasted-text = Texto colado
|
||||
pasted-video = Vídeo colado
|
||||
copy-to-title = Selecione o destino da cópia
|
||||
copy-to-button-label = Copiar
|
||||
move-to-title = Selecione o destino da movimentação
|
||||
|
|
@ -433,6 +433,7 @@ move-to-button-label = Mover
|
|||
copy-to = Copiar para...
|
||||
move-to = Mover para...
|
||||
keywords = Pasta;Gerenciador;
|
||||
show-recents = Pasta Recentes na barra lateral
|
||||
show-recents = Pasta de recentes na barra lateral
|
||||
clear-recents-history = Limpar histórico de recentes
|
||||
copy-path = Copiar caminho
|
||||
mixed = Misto
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ dark = Тёмная
|
|||
light = Светлая
|
||||
# Context menu
|
||||
add-to-sidebar = Добавить на боковую панель
|
||||
compress = Сжать
|
||||
compress = Сжать...
|
||||
extract-here = Распаковать
|
||||
new-file = Новый файл…
|
||||
new-folder = Новая папка…
|
||||
|
|
@ -380,3 +380,4 @@ keywords = Папка;Менеджер;
|
|||
show-recents = «Недавние документы» в бок. панели
|
||||
clear-recents-history = Очистить историю недавних
|
||||
copy-path = Копировать путь
|
||||
mixed = Смешанные
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ single-click = Відкривати одним клацанням
|
|||
type-to-search = Введіть для пошуку
|
||||
type-to-search-recursive = Шукає у поточній теці та всіх підтеках
|
||||
type-to-search-enter-path = Вводить шлях до каталогу або файлу
|
||||
compress = Стиснути
|
||||
compress = Стиснути...
|
||||
delete-permanently = Остаточно видалити
|
||||
eject = Безпечно вилучити
|
||||
extract-here = Видобути
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ recents = 最近访问
|
|||
undo = 撤销
|
||||
today = 今天
|
||||
# Desktop view options
|
||||
desktop-view-options = 桌面视图选项...
|
||||
desktop-view-options = 桌面视图选项…
|
||||
show-on-desktop = 在桌面显示
|
||||
desktop-folder-content = 桌面文件夹内容
|
||||
mounted-drives = 已装载驱动器
|
||||
|
|
@ -31,7 +31,7 @@ operations-running =
|
|||
正在进行 { $running } { $running ->
|
||||
[one] 个操作
|
||||
*[other] 个操作
|
||||
}({ $percent }%)...
|
||||
}({ $percent }%)…
|
||||
operations-running-finished =
|
||||
正在进行 { $running } { $running ->
|
||||
[one] 个操作
|
||||
|
|
@ -50,7 +50,7 @@ create-archive = 创建压缩包
|
|||
## Extract Dialog
|
||||
|
||||
extract-password-required = 需要密码
|
||||
extract-to = 提取到...
|
||||
extract-to = 提取到…
|
||||
extract-to-title = 提取到文件夹
|
||||
|
||||
## Empty Trash Dialog
|
||||
|
|
@ -317,7 +317,7 @@ unknown-folder = 未知文件夹
|
|||
|
||||
## Open with
|
||||
|
||||
menu-open-with = 打开方式...
|
||||
menu-open-with = 打开方式…
|
||||
default-app = { $name }(默认)
|
||||
|
||||
## Show details
|
||||
|
|
@ -329,7 +329,7 @@ item-size = 文件大小:{ $size }
|
|||
item-created = 创建于:{ $created }
|
||||
item-modified = 修改于:{ $modified }
|
||||
item-accessed = 访问于:{ $accessed }
|
||||
calculating = 计算中...
|
||||
calculating = 计算中…
|
||||
|
||||
## Settings
|
||||
|
||||
|
|
@ -355,8 +355,8 @@ compress = 压缩…
|
|||
delete-permanently = 永久删除
|
||||
eject = 弹出
|
||||
extract-here = 解压到此处
|
||||
new-file = 新建文件...
|
||||
new-folder = 新建文件夹...
|
||||
new-file = 新建文件…
|
||||
new-folder = 新建文件夹…
|
||||
open-in-terminal = 在终端模拟器中打开
|
||||
move-to-trash = 移动到回收站
|
||||
restore-from-trash = 从回收站中还原
|
||||
|
|
@ -369,9 +369,9 @@ remove-from-recents = 从最近访问中移除
|
|||
|
||||
## Desktop
|
||||
|
||||
change-wallpaper = 更改壁纸...
|
||||
desktop-appearance = 桌面外观...
|
||||
display-settings = 显示设置...
|
||||
change-wallpaper = 更改壁纸…
|
||||
desktop-appearance = 桌面外观…
|
||||
display-settings = 显示设置…
|
||||
|
||||
# Menu
|
||||
|
||||
|
|
@ -382,7 +382,7 @@ file = 文件
|
|||
new-tab = 新建标签
|
||||
new-window = 新建窗口
|
||||
reload-folder = 刷新文件夹
|
||||
rename = 重命名...
|
||||
rename = 重命名…
|
||||
close-tab = 关闭标签
|
||||
quit = 退出
|
||||
|
||||
|
|
@ -405,7 +405,7 @@ list-view = 列表视图
|
|||
show-hidden-files = 显示隐藏文件
|
||||
list-directories-first = 优先列出目录
|
||||
gallery-preview = 图库预览
|
||||
menu-settings = 设置...
|
||||
menu-settings = 设置…
|
||||
menu-about = 关于 COSMIC 文件…
|
||||
|
||||
## Sort
|
||||
|
|
@ -435,4 +435,5 @@ comment = COSMIC 桌面的文件管理器
|
|||
keywords = 文件夹;管理器;
|
||||
clear-recents-history = 清除最近访问历史
|
||||
copy-path = 复制文件路径
|
||||
show-recents = 侧边栏显示最近访问
|
||||
show-recents = 侧边栏中的最近访问
|
||||
mixed = 混合
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
cosmic-files = COSMIC 檔案總管
|
||||
cosmic-files = COSMIC 檔案
|
||||
empty-folder = 空資料夾
|
||||
empty-folder-hidden = 空資料夾(包含隱藏項目)
|
||||
no-results = 找不到結果
|
||||
filesystem = 檔案系統
|
||||
home = 主目錄
|
||||
home = 家目錄
|
||||
networks = 網路
|
||||
notification-in-progress = 檔案操作正在進行中。
|
||||
notification-in-progress = 檔案操作正在進行中
|
||||
trash = 垃圾桶
|
||||
recents = 最近使用
|
||||
undo = 復原
|
||||
|
|
@ -25,7 +25,7 @@ create-archive = 建立壓縮檔案
|
|||
## Empty Trash Dialog
|
||||
|
||||
empty-trash = 清空垃圾桶
|
||||
empty-trash-warning = 你確定要永久刪除垃圾桶中的所有項目嗎?
|
||||
empty-trash-warning = 垃圾桶中的項目將被永久刪除
|
||||
|
||||
## New File/Folder Dialog
|
||||
|
||||
|
|
@ -33,16 +33,16 @@ create-new-file = 建立新檔案
|
|||
create-new-folder = 建立新資料夾
|
||||
file-name = 檔案名稱
|
||||
folder-name = 資料夾名稱
|
||||
file-already-exists = 已存在同名檔案。
|
||||
folder-already-exists = 已存在同名資料夾。
|
||||
name-hidden = 以「.」開頭的名稱將會被隱藏。
|
||||
name-invalid = 名稱不能是 「{ $filename }」。
|
||||
name-no-slashes = 名稱不能包含斜線。
|
||||
file-already-exists = 相同名稱的檔案已經存在
|
||||
folder-already-exists = 相同名稱的資料夾已經存在
|
||||
name-hidden = 以「.」開頭的名稱將會被隱藏
|
||||
name-invalid = 名稱不能是「{ $filename }」
|
||||
name-no-slashes = 名稱不能包含斜線
|
||||
|
||||
## Open/Save Dialog
|
||||
|
||||
cancel = 取消
|
||||
create = 建立
|
||||
create = 創作
|
||||
open = 開啟
|
||||
open-file = 開啟檔案
|
||||
open-folder = 開啟資料夾
|
||||
|
|
@ -62,12 +62,12 @@ rename-folder = 重新命名資料夾
|
|||
## Replace Dialog
|
||||
|
||||
replace = 取代
|
||||
replace-title = 檔案「{ $filename }」已存在於此位置。
|
||||
replace-title = 「{ $filename }」已存在於此位置
|
||||
replace-warning = 你要取代它嗎?取代將覆蓋其內容。
|
||||
replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。
|
||||
original-file = 原始檔案
|
||||
replace-with = 取代為
|
||||
apply-to-all = 套用至所有項目
|
||||
apply-to-all = 套用至全部
|
||||
keep-both = 保留兩者
|
||||
skip = 跳過
|
||||
|
||||
|
|
@ -95,13 +95,13 @@ network-drive-description =
|
|||
伺服器地址包括協定前綴和地址。
|
||||
範例:ssh://192.168.0.1, ftp://[2001:db8::1]
|
||||
network-drive-schemes =
|
||||
可用協定,前綴
|
||||
AppleTalk,afp://
|
||||
檔案傳輸協定,ftp:// 或 ftps://
|
||||
網路檔案系統,nfs://
|
||||
伺服器訊息區塊,smb://
|
||||
SSH 檔案傳輸協定,sftp:// 或 ssh://
|
||||
WebDav,dav:// 或 davs://
|
||||
可用協定,前綴
|
||||
AppleTalk,afp://
|
||||
檔案傳輸協定,ftp:// 或 ftps://
|
||||
網路檔案系統,nfs://
|
||||
伺服器訊息區塊,smb://
|
||||
SSH 檔案傳輸協定,sftp:// 或 ssh://
|
||||
WebDav,dav:// 或 davs://
|
||||
network-drive-error = 無法存取網路磁碟機
|
||||
password = 密碼
|
||||
remember-password = 記住密碼
|
||||
|
|
@ -112,7 +112,7 @@ username = 使用者名稱
|
|||
|
||||
edit-history = 編輯歷史
|
||||
history = 歷史紀錄
|
||||
no-history = 沒有歷史項目。
|
||||
no-history = 無歷史記錄項目。
|
||||
pending = 待處理
|
||||
failed = 失敗
|
||||
complete = 完成
|
||||
|
|
@ -133,51 +133,51 @@ copying =
|
|||
正在複製 { $items } { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從「{ $from }」到「{ $to }」({ $progress })...
|
||||
}從「{ $from }」到「{ $to }」({ $progress })...
|
||||
copied =
|
||||
已複製 { $items } { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
}從「{ $from }」到「{ $to }」
|
||||
emptying-trash = 正在清空{ trash }({ $progress })…
|
||||
emptied-trash = 已清空{ trash }
|
||||
emptying-trash = 正在清空 { trash }({ $progress })…
|
||||
emptied-trash = 已經清空 { trash }
|
||||
extracting =
|
||||
正在解壓縮 { $items } 項目 { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從「{ $from }」到「{ $to }」({ $progress })...
|
||||
}從「{ $from }」至「{ $to }」({ $progress })...
|
||||
extracted =
|
||||
已解壓縮 { $items } 項目 { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從 { $from } 到 { $to }
|
||||
}從「{ $from }」到「{ $to }」
|
||||
moving =
|
||||
正在移動 { $items } { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從「{ $from }」到「{ $to }」({ $progress })...
|
||||
}從「{ $from }」到「{ $to }」({ $progress })...
|
||||
moved =
|
||||
已移動 { $items } { $items ->
|
||||
已經移動 { $items } { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從「{ $from }」到「{ $to }」
|
||||
renaming = 正在重新命名「{ $from }」為「{ $to }」
|
||||
renamed = 已重新命名 { $from } 為 { $to }
|
||||
} 從「{ $from }」至「{ $to }」
|
||||
renaming = 正在重新命名「{ $from }」至「{ $to }」
|
||||
renamed = 已經重新命名「{ $from }」至「{ $to }」
|
||||
restoring =
|
||||
正在還原 { $items } 項目 { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從{ trash }({ $progress })...
|
||||
}自 { trash } ({ $progress })...
|
||||
restored =
|
||||
已還原 { $items } 項目 { $items ->
|
||||
已經還原 { $items } 項目 { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
} 從{ trash }
|
||||
unknown-folder = 未知資料夾
|
||||
}從 { trash }
|
||||
unknown-folder = 不明資料夾
|
||||
|
||||
## Open with
|
||||
|
||||
menu-open-with = 開啟方式...
|
||||
menu-open-with = 開啟檔案...
|
||||
default-app = { $name } (預設)
|
||||
|
||||
## Show details
|
||||
|
|
@ -192,15 +192,15 @@ settings = 設定
|
|||
|
||||
appearance = 外觀
|
||||
theme = 主題
|
||||
match-desktop = 與桌面一致
|
||||
dark = 暗色模式
|
||||
light = 亮色模式
|
||||
match-desktop = 符合桌面
|
||||
dark = 深色
|
||||
light = 淺色
|
||||
# Context menu
|
||||
add-to-sidebar = 加入側邊欄
|
||||
compress = 壓縮
|
||||
extract-here = 解壓縮至此
|
||||
new-file = 新檔案...
|
||||
new-folder = 新資料夾...
|
||||
add-to-sidebar = 添加至側邊欄
|
||||
compress = 壓縮…
|
||||
extract-here = 解壓縮
|
||||
new-file = 新建檔案...
|
||||
new-folder = 新建資料夾...
|
||||
open-in-terminal = 在終端機中開啟
|
||||
move-to-trash = 移動至垃圾桶
|
||||
restore-from-trash = 從垃圾桶還原
|
||||
|
|
@ -215,8 +215,8 @@ sort-by-size = 依大小排序
|
|||
## File
|
||||
|
||||
file = 檔案
|
||||
new-tab = 新分頁
|
||||
new-window = 新視窗
|
||||
new-tab = 新建分頁
|
||||
new-window = 新建視窗
|
||||
rename = 重新命名...
|
||||
close-tab = 關閉分頁
|
||||
quit = 退出
|
||||
|
|
@ -238,36 +238,36 @@ view = 檢視
|
|||
grid-view = 網格檢視
|
||||
list-view = 列表檢視
|
||||
show-hidden-files = 顯示隱藏檔案
|
||||
list-directories-first = 優先列出目錄
|
||||
list-directories-first = 目錄優先列出
|
||||
menu-settings = 設定...
|
||||
menu-about = 關於 COSMIC 檔案總管...
|
||||
menu-about = 關於 COSMIC 檔案...
|
||||
|
||||
## Sort
|
||||
|
||||
sort = 排序
|
||||
sort-a-z = A-Z
|
||||
sort-z-a = Z-A
|
||||
sort-newest-first = 最新的在前
|
||||
sort-oldest-first = 最舊的在前
|
||||
sort-smallest-to-largest = 由小至大
|
||||
sort-largest-to-smallest = 由大至小
|
||||
sort-newest-first = 最新優先
|
||||
sort-oldest-first = 最舊優先
|
||||
sort-smallest-to-largest = 從小到大
|
||||
sort-largest-to-smallest = 從大到小
|
||||
deleted =
|
||||
已刪除 { $items } { $items ->
|
||||
已經刪除 { $items } { $items ->
|
||||
[one] 項目
|
||||
*[other] 項目
|
||||
}從{ trash }
|
||||
}從 { trash }
|
||||
permanently-deleting =
|
||||
正在永久刪除 { $items } { $items ->
|
||||
[one] 项目
|
||||
*[other] 项目
|
||||
}
|
||||
permanently-deleted =
|
||||
已永久刪除 { $items } { $items ->
|
||||
已經永久刪除 { $items } { $items ->
|
||||
[one] 项目
|
||||
*[other] 项目
|
||||
}
|
||||
removing-from-recents =
|
||||
正在從{ recents }中移除 { $items } { $items ->
|
||||
正在從 { recents } 中移除 { $items } { $items ->
|
||||
[one] 项目
|
||||
*[other] 项目
|
||||
}
|
||||
|
|
@ -275,9 +275,112 @@ deleting =
|
|||
正在刪除 { $items } { $items ->
|
||||
[one] 项目
|
||||
*[other] 项目
|
||||
}從{ trash }({ $progress })…
|
||||
}從 { trash }({ $progress })…
|
||||
removed-from-recents =
|
||||
已從{ recents }中移除 { $items } { $items ->
|
||||
已經從 { recents } 中移除 { $items } { $items ->
|
||||
[one] 项目
|
||||
*[other] 项目
|
||||
}
|
||||
repository = 軟體庫源
|
||||
desktop-view-options = 桌面檢視選項...
|
||||
show-on-desktop = 顯示在桌面
|
||||
desktop-folder-content = 桌面資料夾內容
|
||||
mounted-drives = 已經掛載的磁碟機
|
||||
trash-folder-icon = 垃圾桶圖示
|
||||
trashed-on = 遺棄時間
|
||||
icon-size-and-spacing = 圖示大小與間距
|
||||
icon-size = 圖示大小
|
||||
grid-spacing = 網格間距
|
||||
details = 詳情
|
||||
dismiss = 撤停訊息
|
||||
delete = 刪除
|
||||
remove = 移除
|
||||
support = 支援
|
||||
cancelled = 已取消
|
||||
keywords = 資料夾;管理器;
|
||||
empty-trash-title = 清空垃圾桶?
|
||||
pause = 暫停
|
||||
resume = 繼續
|
||||
extract-password-required = 需要密碼
|
||||
extract-to = 解壓縮至...
|
||||
extract-to-title = 解壓縮至資料夾
|
||||
mount-error = 無法存取磁碟機
|
||||
open-with-title = 您要如何開啟「{ $name }」?
|
||||
browse-store = 瀏覽 { $store }
|
||||
other-apps = 其他應用程式
|
||||
related-apps = 相關應用程式
|
||||
permanently-delete-question = 永久刪除?
|
||||
set-executable-and-launch = 設定為可以執行並啟動
|
||||
read-only = 唯讀
|
||||
read-execute = 讀取和執行
|
||||
read-write = 讀取和寫入
|
||||
read-write-execute = 讀取、寫入和執行
|
||||
favorite-path-error = 開啟目錄時發生錯誤
|
||||
set-executable-and-launch-description = 您是否要將「{ $name }」設為可執行並啟動它?
|
||||
set-and-launch = 設定並啟動
|
||||
none = 無
|
||||
execute-only = 僅執行
|
||||
write-only = 僅寫入
|
||||
write-execute = 寫入和執行
|
||||
operations-running =
|
||||
{ $running } { $running ->
|
||||
[one] 個操作
|
||||
*[other] 個操作
|
||||
}正在執行({ $percent }%)...
|
||||
operations-running-finished =
|
||||
{ $running } { $running ->
|
||||
[one] 個操作
|
||||
*[other] 個操作
|
||||
}正在執行({ $percent }%), { $finished } 個已經完成...
|
||||
permanently-delete-warning = 「{ $target }」將被永久刪除。此操作無法復原。
|
||||
open-with = 開啟檔案
|
||||
selected-items = 已經選定 { $items } 個項目
|
||||
copy-to-title = 選擇複製目的地
|
||||
copy-to-button-label = 複製
|
||||
move-to-title = 選擇移動目的地
|
||||
move-to-button-label = 移動
|
||||
keep = 保留
|
||||
progress = { $percent }%
|
||||
progress-cancelled = { $percent }%,已經取消
|
||||
progress-failed = { $percent }%,失敗
|
||||
progress-paused = { $percent }%,已經暫停
|
||||
favorite-path-error-description =
|
||||
無法開啟「{ $path }」
|
||||
「{ $path }」可能不存在,或您可能沒有權限開啟它。
|
||||
|
||||
您是否要將它從側邊欄移除?
|
||||
comment = COSMIC 桌面檔案管理器
|
||||
pasted-image = 已經貼上的圖片
|
||||
pasted-text = 已經貼上的文字
|
||||
pasted-video = 已經貼上的影片
|
||||
sort-by-trashed = 依丟入時間排序
|
||||
calculating = 計算中...
|
||||
single-click = 點按以開啟
|
||||
type-to-search = 輸入進行搜尋
|
||||
type-to-search-recursive = 搜尋目前資料夾及全部子資料夾
|
||||
type-to-search-enter-path = 輸入目錄或檔案的路徑
|
||||
delete-permanently = 永久刪除
|
||||
eject = 彈出
|
||||
remove-from-recents = 從最近項目中移除
|
||||
change-wallpaper = 變更桌布...
|
||||
desktop-appearance = 桌面外觀...
|
||||
display-settings = 顯示設定...
|
||||
reload-folder = 重新載入資料夾
|
||||
gallery-preview = 圖庫預覽
|
||||
type = 類型:{ $mime }
|
||||
items = 項目:{ $items }
|
||||
item-size = 大小:{ $size }
|
||||
item-created = 建立時間:{ $created }
|
||||
item-modified = 修改時間:{ $modified }
|
||||
item-accessed = 存取時間:{ $accessed }
|
||||
type-to-search-select = 選取第一個符合條件的檔案或資料夾
|
||||
copy-to = 複製至...
|
||||
move-to = 移動至...
|
||||
show-recents = 側邊欄中的最近使用資料夾
|
||||
clear-recents-history = 清除最近使用歷史記錄
|
||||
copy-path = 複製路徑
|
||||
setting-executable-and-launching = 設定「{ $name }」為可以執行並進行啟動
|
||||
set-executable-and-launched = 設定「{ $name }」為可以執行並已經啟動
|
||||
setting-permissions = 設定「{ $name }」的權限至 { $mode }
|
||||
set-permissions = 設定「{ $name }」的權限至 { $mode }
|
||||
mixed = 混合
|
||||
|
|
|
|||
58
src/app.rs
58
src/app.rs
|
|
@ -18,9 +18,7 @@ use cosmic::{
|
|||
Application, ApplicationExt, Element,
|
||||
app::{self, Core, Task, context_drawer},
|
||||
cosmic_config::{self, ConfigSet},
|
||||
cosmic_theme,
|
||||
desktop::fde::DesktopEntry,
|
||||
executor,
|
||||
cosmic_theme, executor,
|
||||
iced::core::widget::operation::focusable::unfocus,
|
||||
iced::runtime::{clipboard, task},
|
||||
iced::widget::{button::focus, scrollable::AbsoluteOffset},
|
||||
|
|
@ -99,6 +97,7 @@ use crate::{
|
|||
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,
|
||||
SearchLocation, Tab,
|
||||
},
|
||||
trash::{Trash, TrashExt},
|
||||
zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
|
||||
};
|
||||
|
||||
|
|
@ -834,9 +833,12 @@ impl App {
|
|||
|
||||
// First launch apps that can be launched directly
|
||||
if mime == "application/x-desktop" {
|
||||
// Try opening desktop application
|
||||
Self::launch_desktop_entries(&paths);
|
||||
continue;
|
||||
#[cfg(feature = "desktop")]
|
||||
{
|
||||
// Try opening desktop application
|
||||
Self::launch_desktop_entries(&paths);
|
||||
continue;
|
||||
}
|
||||
} else if mime == "application/x-executable" || mime == "application/vnd.appimage" {
|
||||
// Try opening executable
|
||||
for path in paths {
|
||||
|
|
@ -897,7 +899,10 @@ impl App {
|
|||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
|
||||
use cosmic::desktop::fde::DesktopEntry;
|
||||
|
||||
for path in paths.iter().map(AsRef::as_ref) {
|
||||
match DesktopEntry::from_path::<&str>(path, None) {
|
||||
Ok(entry) => match entry.exec() {
|
||||
|
|
@ -1794,7 +1799,7 @@ impl App {
|
|||
|
||||
nav_model = nav_model.insert(|b| {
|
||||
b.text(fl!("trash"))
|
||||
.icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
|
||||
.icon(icon::icon(Trash::icon_symbolic(16)))
|
||||
.data(Location::Trash)
|
||||
.divider_above()
|
||||
});
|
||||
|
|
@ -2655,9 +2660,7 @@ impl Application for App {
|
|||
));
|
||||
}
|
||||
|
||||
if matches!(location_opt, Some(Location::Trash))
|
||||
&& !trash::os_limited::is_empty().unwrap_or(true)
|
||||
{
|
||||
if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() {
|
||||
items.push(cosmic::widget::menu::Item::Button(
|
||||
fl!("empty-trash"),
|
||||
None,
|
||||
|
|
@ -4237,10 +4240,8 @@ impl Application for App {
|
|||
.is_some_and(|loc| matches!(loc, Location::Trash))
|
||||
});
|
||||
if let Some(entity) = maybe_entity {
|
||||
self.nav_model.icon_set(
|
||||
entity,
|
||||
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
|
||||
);
|
||||
self.nav_model
|
||||
.icon_set(entity, icon::icon(Trash::icon_symbolic(16)));
|
||||
}
|
||||
|
||||
return Task::batch([self.rescan_trash(), self.update_desktop()]);
|
||||
|
|
@ -4262,12 +4263,14 @@ impl Application for App {
|
|||
.collect();
|
||||
if !selected.is_empty() {
|
||||
//TODO: batch rename
|
||||
let tasks = selected
|
||||
let mut last_name = String::new();
|
||||
let tasks: Vec<_> = selected
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let parent = path.parent()?.to_path_buf();
|
||||
let name = path.file_name()?.to_str()?.to_string();
|
||||
let dir = path.is_dir();
|
||||
last_name = name.clone();
|
||||
Some(self.dialog_pages.push_back(DialogPage::RenameItem {
|
||||
from: path,
|
||||
parent,
|
||||
|
|
@ -4275,9 +4278,15 @@ impl Application for App {
|
|||
dir,
|
||||
}))
|
||||
})
|
||||
.chain(std::iter::once(widget::text_input::focus(
|
||||
.collect();
|
||||
let tasks = tasks.into_iter().chain([
|
||||
widget::text_input::focus(self.dialog_text_input.clone()),
|
||||
widget::text_input::select_until_last(
|
||||
self.dialog_text_input.clone(),
|
||||
)));
|
||||
&last_name,
|
||||
'.',
|
||||
),
|
||||
]);
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
}
|
||||
|
|
@ -6151,6 +6160,7 @@ impl Application for App {
|
|||
.into(),
|
||||
widget::text_input("", name.as_str())
|
||||
.id(self.dialog_text_input.clone())
|
||||
.double_click_select_delimiter('.')
|
||||
.on_input(move |name| {
|
||||
Message::DialogUpdate(DialogPage::RenameItem {
|
||||
from: from.clone(),
|
||||
|
|
@ -6794,14 +6804,7 @@ impl Application for App {
|
|||
},
|
||||
);
|
||||
|
||||
// TODO: Trash watching support for Windows, macOS, and other OSes
|
||||
#[cfg(all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "android")
|
||||
))]
|
||||
match (watcher_res, trash::os_limited::trash_folders()) {
|
||||
match (watcher_res, Trash::folders()) {
|
||||
(Ok(mut watcher), Ok(trash_bins)) => {
|
||||
// Watch the "bins" themselves as well as the files folder where
|
||||
// trashed items are placed. This allows us to avoid recursively
|
||||
|
|
@ -6958,8 +6961,7 @@ impl Application for App {
|
|||
|_| {
|
||||
stream::channel(
|
||||
1,
|
||||
move |msg_tx: futures::channel::mpsc::Sender<_>| async move {
|
||||
let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));
|
||||
move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
match notify_rust::Notification::new()
|
||||
.summary(&fl!("notification-in-progress"))
|
||||
|
|
@ -6969,8 +6971,6 @@ impl Application for App {
|
|||
Ok(notification) => {
|
||||
let _ = futures::executor::block_on(async {
|
||||
msg_tx
|
||||
.lock()
|
||||
.await
|
||||
.send(Message::Notification(Arc::new(
|
||||
Mutex::new(notification),
|
||||
)))
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
|||
if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
|
||||
let mut target = Vec::with_capacity(file.size() as usize);
|
||||
file.read_to_end(&mut target)?;
|
||||
// File no longer needed, drop to allow reading target on windows
|
||||
drop(file);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -199,11 +201,15 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
|||
#[cfg(windows)]
|
||||
{
|
||||
let Ok(target) = String::from_utf8(target) else {
|
||||
return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
|
||||
return Err(ZipError::InvalidArchive(
|
||||
"Invalid UTF-8 as symlink target".into(),
|
||||
));
|
||||
};
|
||||
let target = target.into_boxed_str();
|
||||
let target_is_dir_from_archive =
|
||||
archive.shared.files.contains_key(&target) && is_dir(&target);
|
||||
let target_is_dir_from_archive = match password {
|
||||
None => archive.by_name(&target),
|
||||
Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()),
|
||||
}
|
||||
.map_or(false, |x| x.is_dir());
|
||||
let target_path = directory.as_ref().join(OsString::from(target.to_string()));
|
||||
let target_is_dir = if target_is_dir_from_archive {
|
||||
true
|
||||
|
|
|
|||
77
src/channel.rs
Normal file
77
src/channel.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
/// Create a channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.
|
||||
pub fn channel<Message>() -> (Sender<Message>, Receiver<Message>) {
|
||||
let channel = Arc::new(Channel {
|
||||
queue: Mutex::new(VecDeque::default()),
|
||||
notify: tokio::sync::Notify::const_new(),
|
||||
closed: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
(Sender(channel.clone()), Receiver(channel))
|
||||
}
|
||||
|
||||
/// A channel backed by `tokio::sync::Notify` and a sync mutex with a vec deque.
|
||||
struct Channel<Message> {
|
||||
pub(self) queue: Mutex<VecDeque<Message>>,
|
||||
/// Set when a new message has been stored.
|
||||
pub(self) notify: tokio::sync::Notify,
|
||||
/// Set when the receiver is dropped.
|
||||
pub(self) closed: AtomicBool,
|
||||
}
|
||||
|
||||
pub struct Sender<Message>(Arc<Channel<Message>>);
|
||||
|
||||
impl<Message> Sender<Message> {
|
||||
pub fn send(&self, message: Message) {
|
||||
self.0.queue.lock().unwrap().push_back(message);
|
||||
self.0.notify.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Drop for Sender<Message> {
|
||||
fn drop(&mut self) {
|
||||
self.0.closed.store(true, Ordering::SeqCst);
|
||||
self.0.notify.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Receiver<Message>(Arc<Channel<Message>>);
|
||||
|
||||
impl<Message> Receiver<Message> {
|
||||
/// Returns a value until the sender is dropped.
|
||||
pub async fn recv(&self) -> Option<Message> {
|
||||
loop {
|
||||
{
|
||||
let mut queue = self.0.queue.lock().unwrap();
|
||||
if let Some(value) = queue.pop_front() {
|
||||
if queue.capacity() - queue.len() > 32 {
|
||||
let capacity = queue.len().next_power_of_two();
|
||||
queue.shrink_to(capacity);
|
||||
}
|
||||
drop(queue);
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
if self.0.closed.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.0.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<Message> {
|
||||
self.0.queue.lock().unwrap().pop_front()
|
||||
}
|
||||
}
|
||||
|
|
@ -441,13 +441,8 @@ impl<M: Send + 'static> Dialog<M> {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
enum DialogPage {
|
||||
NewFolder {
|
||||
parent: PathBuf,
|
||||
name: String,
|
||||
},
|
||||
Replace {
|
||||
filename: String,
|
||||
},
|
||||
NewFolder { parent: PathBuf, name: String },
|
||||
Replace { filename: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -600,6 +595,7 @@ impl App {
|
|||
col = col.push(
|
||||
widget::text_input("", filename)
|
||||
.id(self.filename_id.clone())
|
||||
.double_click_select_delimiter('.')
|
||||
.on_input(Message::Filename)
|
||||
.on_submit(|_| Message::Save(false)),
|
||||
);
|
||||
|
|
@ -1962,6 +1958,16 @@ impl Application for App {
|
|||
if self.search_get().is_some() {
|
||||
return widget::text_input::focus(self.search_id.clone());
|
||||
}
|
||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||
return Task::batch([
|
||||
widget::text_input::focus(self.filename_id.clone()),
|
||||
widget::text_input::select_until_last(
|
||||
self.filename_id.clone(),
|
||||
filename,
|
||||
'.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return widget::text_input::focus(self.filename_id.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -2031,7 +2037,7 @@ impl Application for App {
|
|||
}
|
||||
|
||||
col = col.push(
|
||||
self.tab
|
||||
self.tab
|
||||
.view(&self.key_binds, &self.modifiers, false, &[])
|
||||
.map(Message::TabMessage),
|
||||
);
|
||||
|
|
|
|||
53
src/lib.rs
53
src/lib.rs
|
|
@ -3,14 +3,20 @@
|
|||
|
||||
use cosmic::{app::Settings, iced::Limits};
|
||||
use std::{env, fs, path::PathBuf, process};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::{
|
||||
app::{App, Flags},
|
||||
config::{Config, State},
|
||||
tab::Location,
|
||||
};
|
||||
|
||||
use app::{App, Flags};
|
||||
pub mod app;
|
||||
mod archive;
|
||||
pub mod channel;
|
||||
pub mod clipboard;
|
||||
mod context_action;
|
||||
use config::Config;
|
||||
pub mod config;
|
||||
mod context_action;
|
||||
pub mod dialog;
|
||||
mod key_bind;
|
||||
pub(crate) mod large_image;
|
||||
|
|
@ -23,13 +29,11 @@ mod mounter;
|
|||
mod mouse_area;
|
||||
pub mod operation;
|
||||
mod spawn_detached;
|
||||
use tab::Location;
|
||||
mod zoom;
|
||||
|
||||
use crate::config::State;
|
||||
pub mod tab;
|
||||
mod thumbnail_cacher;
|
||||
mod thumbnailer;
|
||||
pub(crate) mod trash;
|
||||
mod zoom;
|
||||
|
||||
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
|
||||
|
||||
|
|
@ -73,7 +77,22 @@ pub fn is_wayland() -> bool {
|
|||
/// Runs application in desktop mode
|
||||
#[rustfmt::skip]
|
||||
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
let log_format = tracing_subscriber::fmt::format()
|
||||
.pretty()
|
||||
.without_time()
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_target(false)
|
||||
.with_thread_names(true);
|
||||
|
||||
let log_layer = tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(log_format);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_env("RUST_LOG"))
|
||||
.with(log_layer)
|
||||
.init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
|
|
@ -108,7 +127,21 @@ pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
|
|||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
let log_format = tracing_subscriber::fmt::format()
|
||||
.pretty()
|
||||
.with_line_number(true)
|
||||
.with_file(true)
|
||||
.with_target(false)
|
||||
.with_thread_names(true);
|
||||
|
||||
let log_layer = tracing_subscriber::fmt::Layer::default()
|
||||
.with_writer(std::io::stderr)
|
||||
.event_format(log_format);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(log_layer)
|
||||
.init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
|
|
@ -158,7 +191,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
if daemonize {
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
|
||||
match fork::daemon(true, true) {
|
||||
Ok(fork::Fork::Child) => (),
|
||||
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use crate::{
|
|||
config::{Config, ContextActionPreset},
|
||||
fl,
|
||||
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
|
||||
trash::{Trash, TrashExt},
|
||||
};
|
||||
|
||||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||
|
|
@ -192,7 +193,7 @@ pub fn context_menu<'a>(
|
|||
) => {
|
||||
if selected_trash_only {
|
||||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
if !trash::os_limited::is_empty().unwrap_or(true) {
|
||||
if !Trash::is_empty() {
|
||||
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
|
||||
}
|
||||
} else if let Some(entry) = selected_desktop_entry {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ struct MimeIconKey {
|
|||
|
||||
struct MimeIconCache {
|
||||
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
|
||||
#[cfg(unix)]
|
||||
shared_mime_info: xdg_mime::SharedMimeInfo,
|
||||
}
|
||||
|
||||
|
|
@ -26,10 +27,17 @@ impl MimeIconCache {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: FxHashMap::default(),
|
||||
#[cfg(unix)]
|
||||
shared_mime_info: xdg_mime::SharedMimeInfo::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
|
||||
self.cache
|
||||
.entry(key)
|
||||
|
|
@ -53,6 +61,16 @@ impl MimeIconCache {
|
|||
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
|
||||
LazyLock::new(|| Mutex::new(MimeIconCache::new()));
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn mime_for_path(
|
||||
path: impl AsRef<Path>,
|
||||
metadata_opt: Option<&fs::Metadata>,
|
||||
remote: bool,
|
||||
) -> Mime {
|
||||
mime_guess::from_path(path).first_or_octet_stream()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn mime_for_path(
|
||||
path: impl AsRef<Path>,
|
||||
metadata_opt: Option<&fs::Metadata>,
|
||||
|
|
@ -100,8 +118,13 @@ pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn parent_mime_types(_mime: &Mime) -> Option<Vec<Mime>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
|
||||
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
|
||||
|
||||
mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use cosmic::{
|
|||
};
|
||||
use gio::{glib, prelude::*};
|
||||
use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
|
||||
use crate::{
|
||||
|
|
@ -199,6 +199,7 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
|
|||
metadata,
|
||||
hidden,
|
||||
location_opt: Some(location),
|
||||
image_dimensions: None,
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -229,7 +230,10 @@ fn dir_info(uri: &str) -> Result<(String, String, Option<PathBuf>), glib::Error>
|
|||
Ok((resolved_uri, info.display_name().into(), file.path()))
|
||||
}
|
||||
|
||||
fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
|
||||
fn mount_op(
|
||||
uri: String,
|
||||
event_tx: std::sync::Weak<crate::channel::Sender<Event>>,
|
||||
) -> gio::MountOperation {
|
||||
let mount_op = gio::MountOperation::new();
|
||||
mount_op.connect_ask_password(
|
||||
move |mount_op, message, default_user, default_domain, flags| {
|
||||
|
|
@ -252,9 +256,9 @@ fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOp
|
|||
.then_some(false),
|
||||
};
|
||||
let (auth_tx, mut auth_rx) = mpsc::channel(1);
|
||||
event_tx
|
||||
.send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
|
||||
.unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx));
|
||||
}
|
||||
//TODO: async recv?
|
||||
if let Some(auth) = auth_rx.blocking_recv() {
|
||||
if auth.anonymous_opt == Some(true) {
|
||||
|
|
@ -357,37 +361,45 @@ impl Item {
|
|||
|
||||
pub struct Gvfs {
|
||||
command_tx: mpsc::UnboundedSender<Cmd>,
|
||||
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
|
||||
event_rx: Arc<crate::channel::Receiver<Event>>,
|
||||
}
|
||||
|
||||
impl Gvfs {
|
||||
pub fn new() -> Self {
|
||||
//TODO: switch to using gvfs-zbus which will better integrate with async rust
|
||||
let (command_tx, mut command_rx) = mpsc::unbounded_channel();
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (event_tx, event_rx) = crate::channel::channel();
|
||||
let event_tx = Arc::new(event_tx);
|
||||
std::thread::spawn(move || {
|
||||
let main_loop = glib::MainLoop::new(None, false);
|
||||
main_loop.context().spawn_local(async move {
|
||||
let event_tx = Arc::downgrade(&event_tx);
|
||||
let monitor = gio::VolumeMonitor::get();
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
monitor.connect_mount_changed(move |_monitor, mount| {
|
||||
log::info!("mount changed {}", MountExt::name(mount));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
monitor.connect_mount_added(move |_monitor, mount| {
|
||||
log::info!("mount added {}", MountExt::name(mount));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
monitor.connect_mount_removed(move |_monitor, mount| {
|
||||
log::info!("mount removed {}", MountExt::name(mount));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -395,21 +407,27 @@ impl Gvfs {
|
|||
let event_tx = event_tx.clone();
|
||||
monitor.connect_volume_changed(move |_monitor, volume| {
|
||||
log::info!("volume changed {}", VolumeExt::name(volume));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
monitor.connect_volume_added(move |_monitor, volume| {
|
||||
log::info!("volume added {}", VolumeExt::name(volume));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
let event_tx = event_tx.clone();
|
||||
monitor.connect_volume_removed(move |_monitor, volume| {
|
||||
log::info!("volume removed {}", VolumeExt::name(volume));
|
||||
event_tx.send(Event::Changed).unwrap();
|
||||
if let Some(event_tx) = event_tx.upgrade() {
|
||||
event_tx.send(Event::Changed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -419,7 +437,11 @@ impl Gvfs {
|
|||
items_tx.send(items(&monitor, sizes)).await.unwrap();
|
||||
}
|
||||
Cmd::Rescan => {
|
||||
event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
|
||||
let Some(event_tx) = event_tx.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
event_tx.send(Event::Items(items(&monitor, IconSizes::default())));
|
||||
}
|
||||
Cmd::Mount(mounter_item, complete_tx) => {
|
||||
let MounterItem::Gvfs(ref item) = mounter_item else {
|
||||
|
|
@ -471,6 +493,9 @@ impl Gvfs {
|
|||
.ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
||||
.unwrap_or(true);
|
||||
}
|
||||
let Some(event_tx) = event_tx.upgrade() else {
|
||||
return;
|
||||
};
|
||||
event_tx.send(Event::MountResult(updated_item, match res {
|
||||
Ok(()) => {
|
||||
_ = complete_tx.send(Ok(()));
|
||||
|
|
@ -482,7 +507,7 @@ impl Gvfs {
|
|||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||
_ => Err(format!("{err}"))
|
||||
}}
|
||||
})).unwrap();
|
||||
}));
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
|
@ -498,6 +523,9 @@ impl Gvfs {
|
|||
gio::Cancellable::NONE,
|
||||
move |res| {
|
||||
log::info!("network drive {uri}: result {res:?}");
|
||||
let Some(event_tx) = event_tx.upgrade() else {
|
||||
return;
|
||||
};
|
||||
event_tx.send(Event::NetworkResult(uri, match res {
|
||||
Ok(()) => {
|
||||
_ = result_tx.send(Ok(()));
|
||||
|
|
@ -508,7 +536,7 @@ impl Gvfs {
|
|||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||
_ => Err(format!("{err}"))
|
||||
}}
|
||||
})).unwrap();
|
||||
}));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -532,6 +560,9 @@ impl Gvfs {
|
|||
// FIXME sometimes a uri can be mounted and then not recognized as mounted...
|
||||
// seems to be related to uri with a path
|
||||
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap();
|
||||
let Some(event_tx) = event_tx.upgrade() else {
|
||||
return;
|
||||
};
|
||||
event_tx.send(Event::NetworkResult(resolved_uri, match res {
|
||||
Ok(()) => {
|
||||
Ok(true)
|
||||
|
|
@ -540,7 +571,7 @@ impl Gvfs {
|
|||
Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
|
||||
_ => Err(format!("{err}"))
|
||||
}
|
||||
})).unwrap();
|
||||
}));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
|
@ -596,7 +627,7 @@ impl Gvfs {
|
|||
});
|
||||
Self {
|
||||
command_tx,
|
||||
event_rx: Arc::new(Mutex::new(event_rx)),
|
||||
event_rx: Arc::new(event_rx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -670,7 +701,7 @@ impl Mounter for Gvfs {
|
|||
let event_rx = self.event_rx.clone();
|
||||
struct Wrapper {
|
||||
command_tx: mpsc::UnboundedSender<Cmd>,
|
||||
event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
|
||||
event_rx: Arc<crate::channel::Receiver<Event>>,
|
||||
}
|
||||
impl Hash for Wrapper {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
|
|
@ -694,7 +725,7 @@ impl Mounter for Gvfs {
|
|||
MounterMessage,
|
||||
>| async move {
|
||||
command_tx.send(Cmd::Rescan).unwrap();
|
||||
while let Some(event) = event_rx.lock().await.recv().await {
|
||||
while let Some(event) = event_rx.recv().await {
|
||||
match event {
|
||||
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
|
||||
Event::Items(items) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use atomic_float::AtomicF32;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{self, AtomicU16};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum ControllerState {
|
||||
Cancelled,
|
||||
Failed,
|
||||
|
|
@ -11,8 +15,8 @@ pub enum ControllerState {
|
|||
|
||||
#[derive(Debug)]
|
||||
struct ControllerInner {
|
||||
state: Mutex<ControllerState>,
|
||||
progress: Mutex<f32>,
|
||||
state: AtomicU16,
|
||||
progress: AtomicF32,
|
||||
notify: Notify,
|
||||
}
|
||||
|
||||
|
|
@ -27,8 +31,8 @@ impl Default for Controller {
|
|||
Self {
|
||||
primary: true,
|
||||
inner: Arc::new(ControllerInner {
|
||||
state: Mutex::new(ControllerState::Running),
|
||||
progress: Mutex::new(0.0),
|
||||
state: AtomicU16::new(ControllerState::Running.into()),
|
||||
progress: AtomicF32::new(0.0),
|
||||
notify: Notify::new(),
|
||||
}),
|
||||
}
|
||||
|
|
@ -50,19 +54,24 @@ impl Controller {
|
|||
}
|
||||
|
||||
pub fn progress(&self) -> f32 {
|
||||
*self.inner.progress.lock().unwrap()
|
||||
self.inner.progress.load(atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_progress(&self, progress: f32) {
|
||||
*self.inner.progress.lock().unwrap() = progress;
|
||||
self.inner
|
||||
.progress
|
||||
.swap(progress, atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn state(&self) -> ControllerState {
|
||||
*self.inner.state.lock().unwrap()
|
||||
ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed))
|
||||
.unwrap_or(ControllerState::Failed)
|
||||
}
|
||||
|
||||
pub fn set_state(&self, state: ControllerState) {
|
||||
*self.inner.state.lock().unwrap() = state;
|
||||
self.inner
|
||||
.state
|
||||
.store(state.into(), atomic::Ordering::Relaxed);
|
||||
self.inner.notify.notify_waiters();
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +95,35 @@ impl Controller {
|
|||
self.set_state(ControllerState::Paused);
|
||||
}
|
||||
|
||||
/// Returns when the state is paused.
|
||||
///
|
||||
/// Use this to pause futures.
|
||||
pub async fn until_paused(&self) {
|
||||
loop {
|
||||
if matches!(self.state(), ControllerState::Paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns when state is neither paused, cancelled, nor failed.
|
||||
///
|
||||
/// Use this to resume futures.
|
||||
pub async fn until_unpaused(&self) {
|
||||
loop {
|
||||
if !matches!(
|
||||
self.state(),
|
||||
ControllerState::Paused | ControllerState::Cancelled | ControllerState::Failed
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner.notify.notified().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unpause(&self) {
|
||||
if !self.is_cancelled() | !self.is_failed() {
|
||||
self.set_state(ControllerState::Running);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ use zip::AesMode::Aes256;
|
|||
pub use self::controller::{Controller, ControllerState};
|
||||
pub mod controller;
|
||||
|
||||
pub use notifiers::*;
|
||||
mod notifiers;
|
||||
|
||||
pub use self::reader::OpReader;
|
||||
pub mod reader;
|
||||
|
||||
|
|
@ -111,7 +114,7 @@ async fn copy_or_move(
|
|||
);
|
||||
|
||||
// Handle duplicate file names by renaming paths
|
||||
let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
|
||||
let from_to_pairs_iter = paths
|
||||
.into_iter()
|
||||
.zip(std::iter::repeat(to.as_path()))
|
||||
.filter_map(|(from, to)| {
|
||||
|
|
@ -129,36 +132,46 @@ async fn copy_or_move(
|
|||
//TODO: how to handle from missing file name?
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
|
||||
// Attempt quick and simple renames
|
||||
//TODO: allow rename to be used for directories in recursive context?
|
||||
if matches!(method, Method::Move { .. }) {
|
||||
from_to_pairs.retain(|(from, to)| {
|
||||
//TODO: show replace dialog here?
|
||||
if to.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
//TODO: use compio::fs::rename?
|
||||
match fs::rename(from, to) {
|
||||
Ok(()) => {
|
||||
log::info!("renamed {} to {}", from.display(), to.display());
|
||||
false
|
||||
let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) {
|
||||
from_to_pairs_iter
|
||||
.map(|(from, to)| async move {
|
||||
//TODO: show replace dialog here?
|
||||
if to.exists() {
|
||||
return Some((from, to));
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!(
|
||||
"failed to rename {} to {}, fallback to recursive move: {}",
|
||||
from.display(),
|
||||
to.display(),
|
||||
err
|
||||
);
|
||||
true
|
||||
|
||||
match compio::fs::rename(&from, &to).await {
|
||||
Ok(()) => {
|
||||
log::info!("renamed {} to {}", from.display(), to.display());
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!(
|
||||
"failed to rename {} to {}, fallback to recursive move: {}",
|
||||
from.display(),
|
||||
to.display(),
|
||||
err
|
||||
);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect::<cosmic::iced::futures::stream::FuturesOrdered<_>>()
|
||||
.fold(Vec::new(), |mut pairs, pair| async move {
|
||||
if let Some(pair) = pair {
|
||||
pairs.push(pair);
|
||||
}
|
||||
pairs
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
from_to_pairs_iter.collect()
|
||||
};
|
||||
|
||||
let mut context = Context::new(controller.clone());
|
||||
|
||||
|
|
@ -216,7 +229,7 @@ pub async fn sync_to_disk(
|
|||
}
|
||||
}))
|
||||
.buffer_unordered(32)
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<()>()
|
||||
.await;
|
||||
|
||||
// Sync directories to disk
|
||||
|
|
@ -226,7 +239,7 @@ pub async fn sync_to_disk(
|
|||
}
|
||||
}))
|
||||
.buffer_unordered(16)
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<()>()
|
||||
.await;
|
||||
}
|
||||
|
||||
|
|
@ -1112,7 +1125,9 @@ impl Operation {
|
|||
#[cfg(target_os = "macos")]
|
||||
Self::Restore { .. } => {
|
||||
// TODO: add support for macos
|
||||
return OperationError::from_msg("Restoring from trash is not supported on macos");
|
||||
return Err(OperationError::from_msg(
|
||||
"Restoring from trash is not supported on macos",
|
||||
));
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Self::Restore { items } => {
|
||||
|
|
|
|||
58
src/operation/notifiers.rs
Normal file
58
src/operation/notifiers.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2026 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
/// Monitor files which are being written to.
|
||||
pub struct FileWritingNotifier {
|
||||
data: Vec<PathBuf>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
static ACTIVELY_WRITING: LazyLock<Mutex<FileWritingNotifier>> = LazyLock::new(|| {
|
||||
Mutex::new(FileWritingNotifier {
|
||||
data: Vec::new(),
|
||||
notify: Arc::new(Notify::new()),
|
||||
})
|
||||
});
|
||||
|
||||
/// Append path that is being written to.
|
||||
pub fn actively_writing_add(path: PathBuf) {
|
||||
ACTIVELY_WRITING.lock().unwrap().data.push(path);
|
||||
}
|
||||
|
||||
/// Remove path to file that has finished writing and notify waiters.
|
||||
pub fn actively_writing_remove(path: &Path) {
|
||||
let mut guard = ACTIVELY_WRITING.lock().unwrap();
|
||||
guard.data.retain(|p| p != path);
|
||||
guard.notify.notify_waiters();
|
||||
}
|
||||
|
||||
/// Wait until the actively-writing queue is empty or a file has been removed.
|
||||
pub async fn actively_writing_tick() {
|
||||
let notify = (|| {
|
||||
let guard = ACTIVELY_WRITING.lock().unwrap();
|
||||
|
||||
if !guard.data.is_empty() {
|
||||
return Some(guard.notify.clone());
|
||||
}
|
||||
|
||||
None
|
||||
})();
|
||||
|
||||
if let Some(notify) = notify {
|
||||
notify.notified().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a file is being written to. Avoid thumbnail generation until after it is finished.
|
||||
pub fn is_actively_writing_to(path: &Path) -> bool {
|
||||
ACTIVELY_WRITING
|
||||
.lock()
|
||||
.unwrap()
|
||||
.data
|
||||
.iter()
|
||||
.any(|p| p == path)
|
||||
}
|
||||
|
|
@ -1,15 +1,33 @@
|
|||
use compio::BufResult;
|
||||
use compio::buf::{IntoInner, IoBuf};
|
||||
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::time::Instant;
|
||||
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::operation::{OperationError, sync_to_disk};
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
|
||||
use crate::operation::{OperationError, sync_to_disk};
|
||||
use anyhow::Context as AnyhowContext;
|
||||
use compio::BufResult;
|
||||
use compio::buf::{IntoInner, IoBuf};
|
||||
use compio::driver::{ToSharedFd, op::AsyncifyFd};
|
||||
use compio::io::{AsyncReadAt, AsyncWriteAt};
|
||||
use cosmic::iced::futures;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[cfg(feature = "gvfs")]
|
||||
use gio::prelude::FileExtManual;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum GioCopyError {
|
||||
#[error("controller state")]
|
||||
Controller(OperationError),
|
||||
#[cfg(feature = "gvfs")]
|
||||
#[error("gio copy failed")]
|
||||
GLib(#[from] glib::Error),
|
||||
}
|
||||
|
||||
pub enum Method {
|
||||
Copy,
|
||||
|
|
@ -313,136 +331,28 @@ impl Op {
|
|||
})
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
mut progress: Progress,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> {
|
||||
if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {
|
||||
return Ok(true);
|
||||
}
|
||||
match self.kind {
|
||||
OpKind::Copy => {
|
||||
// Remove `to` if overwriting and it is an existing file
|
||||
if self.to.is_file() {
|
||||
match ctx.replace(self).await? {
|
||||
ControlFlow::Continue(to) => {
|
||||
self.to = to;
|
||||
}
|
||||
ControlFlow::Break(ret) => {
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
crate::operation::actively_writing_add(self.to.clone());
|
||||
let result = self.copy(ctx, progress).await;
|
||||
|
||||
if result.is_err() {
|
||||
_ = compio::fs::remove_file(&self.to).await;
|
||||
}
|
||||
|
||||
let (from_file, metadata, mut to_file) = cosmic::iced::futures::try_join!(
|
||||
async {
|
||||
compio::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&self.from)
|
||||
.await
|
||||
},
|
||||
compio::fs::metadata(&self.from),
|
||||
// This is atomic and ensures `to` is not created by any other process
|
||||
async {
|
||||
compio::fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&self.to)
|
||||
.await
|
||||
}
|
||||
)?;
|
||||
|
||||
progress.total_bytes = Some(metadata.len());
|
||||
(ctx.on_progress)(self, &progress);
|
||||
if let Err(err) = to_file.set_permissions(metadata.permissions()).await {
|
||||
// This error is not propagated upwards as some filesystems do not support setting permissions
|
||||
log::warn!(
|
||||
"failed to set permissions for {}: {}",
|
||||
self.to.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent spamming the progress callbacks.
|
||||
let mut last_progress_update = Instant::now();
|
||||
// io_uring/IOCP requires transferring ownership of the buffer to the kernel.
|
||||
let mut buf_in = std::mem::take(&mut ctx.buf);
|
||||
// Track where the current read/write position is at.
|
||||
let mut pos = 0;
|
||||
|
||||
loop {
|
||||
let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;
|
||||
|
||||
let count = match result {
|
||||
Ok(0) => {
|
||||
ctx.buf = buf_out;
|
||||
break;
|
||||
}
|
||||
Ok(count) => count,
|
||||
Err(why) => {
|
||||
ctx.buf = buf_out;
|
||||
return Err(why.into());
|
||||
}
|
||||
};
|
||||
|
||||
let BufResult(result, buf_out_slice) =
|
||||
to_file.write_at(buf_out.slice(..count), pos).await;
|
||||
let buf_out = buf_out_slice.into_inner();
|
||||
|
||||
if let Err(why) = result {
|
||||
ctx.buf = buf_out;
|
||||
return Err(why.into());
|
||||
}
|
||||
|
||||
progress.current_bytes += count as u64;
|
||||
pos += count as u64;
|
||||
|
||||
// Avoid spamming progress messages too early.
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last_progress_update).as_millis() > 49 {
|
||||
last_progress_update = current;
|
||||
(ctx.on_progress)(self, &progress);
|
||||
|
||||
// Also check if the progress was cancelled.
|
||||
if let Err(state) = ctx.controller.check().await {
|
||||
ctx.buf = buf_out;
|
||||
return Err(OperationError::from_state(state, &ctx.controller).into());
|
||||
}
|
||||
}
|
||||
|
||||
buf_in = buf_out;
|
||||
}
|
||||
|
||||
let mut times = fs::FileTimes::new();
|
||||
{
|
||||
use std::os::unix::prelude::MetadataExt;
|
||||
log::info!("{}", metadata.mtime());
|
||||
}
|
||||
if let Ok(time) = metadata.modified() {
|
||||
times = times.set_modified(time);
|
||||
}
|
||||
if let Ok(time) = metadata.accessed() {
|
||||
times = times.set_accessed(time);
|
||||
}
|
||||
//TODO: upstream set_times implementation to compio?
|
||||
{
|
||||
use compio::driver::{ToSharedFd, op::AsyncifyFd};
|
||||
let op =
|
||||
AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {
|
||||
BufResult(file.set_times(times).map(|_| 0), ())
|
||||
});
|
||||
match compio::runtime::submit(op).await.0.map(|_| ()) {
|
||||
Ok(()) => {
|
||||
log::info!("set times for {} to {:?}", self.to.display(), times);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to set times for {}: {}", self.to.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::operation::actively_writing_remove(&self.to);
|
||||
return result;
|
||||
}
|
||||
OpKind::Move { cross_device_copy } => {
|
||||
// Do not clean up if cross_device_copy is set
|
||||
if cross_device_copy {
|
||||
self.skipped.cleanup.set(true);
|
||||
}
|
||||
|
||||
// Remove `to` if overwriting and it is an existing file
|
||||
if self.to.is_file() {
|
||||
match ctx.replace(self).await? {
|
||||
|
|
@ -520,4 +430,268 @@ impl Op {
|
|||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
mut progress: Progress,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
// Remove `to` if overwriting and it is an existing file
|
||||
if self.to.is_file() {
|
||||
match ctx.replace(self).await? {
|
||||
ControlFlow::Continue(to) => {
|
||||
self.to = to;
|
||||
}
|
||||
ControlFlow::Break(ret) => {
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (from_file, metadata, to_file) = cosmic::iced::futures::join!(
|
||||
async {
|
||||
compio::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(&self.from)
|
||||
.await
|
||||
.with_context(|| format!("failed to open {} for reading", self.from.display(),))
|
||||
},
|
||||
async { compio::fs::metadata(&self.from).await.ok() },
|
||||
// This is atomic and ensures `to` is not created by any other process
|
||||
async {
|
||||
compio::fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&self.to)
|
||||
.await
|
||||
.with_context(|| format!("failed to open {} for writing", self.to.display()))
|
||||
}
|
||||
);
|
||||
|
||||
let from_file = from_file?;
|
||||
let mut to_file = to_file?;
|
||||
progress.total_bytes = metadata.as_ref().map(|m| m.len());
|
||||
(ctx.on_progress)(self, &progress);
|
||||
|
||||
if let Some(metadata) = metadata.as_ref() {
|
||||
if let Err(why) = to_file.set_permissions(metadata.permissions()).await {
|
||||
// This error is not propagated upwards as some filesystems do not support setting permissions
|
||||
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
|
||||
tracing::warn!(?why, "failed to set permissions for {}", self.to.display(),);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent spamming the progress callbacks.
|
||||
let mut last_progress_update = Instant::now();
|
||||
// io_uring/IOCP requires transferring ownership of the buffer to the kernel.
|
||||
let mut buf_in = std::mem::take(&mut ctx.buf);
|
||||
// Track where the current read/write position is at.
|
||||
let mut pos = 0;
|
||||
|
||||
loop {
|
||||
let BufResult(result, buf_out) = from_file.read_at(buf_in, pos).await;
|
||||
|
||||
let count = match result {
|
||||
Ok(0) => {
|
||||
buf_in = buf_out;
|
||||
break;
|
||||
}
|
||||
Ok(count) => count,
|
||||
Err(why) => {
|
||||
ctx.buf = buf_out;
|
||||
tracing::error!("failed to read: {:?}", why);
|
||||
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
||||
return Err(why).context("failed to read")?;
|
||||
}
|
||||
};
|
||||
|
||||
let BufResult(result, buf_out_slice) =
|
||||
to_file.write_at(buf_out.slice(..count), pos).await;
|
||||
let buf_out = buf_out_slice.into_inner();
|
||||
|
||||
if let Err(why) = result {
|
||||
#[cfg(feature = "gvfs")]
|
||||
if let std::io::ErrorKind::Unsupported = why.kind() {
|
||||
ctx.buf = buf_out;
|
||||
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
||||
return self
|
||||
.gio_file_copy(ctx, progress)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.map_err(Into::into);
|
||||
}
|
||||
|
||||
tracing::error!("failed to write: {:?}", why);
|
||||
ctx.buf = buf_out;
|
||||
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
||||
return Err(why).context("failed to write")?;
|
||||
}
|
||||
|
||||
progress.current_bytes += count as u64;
|
||||
pos += count as u64;
|
||||
|
||||
// Avoid spamming progress messages too early.
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last_progress_update).as_millis() > 49 {
|
||||
last_progress_update = current;
|
||||
(ctx.on_progress)(self, &progress);
|
||||
|
||||
// Also check if the progress was cancelled.
|
||||
if let Err(state) = ctx.controller.check().await {
|
||||
ctx.buf = buf_out;
|
||||
tracing::warn!(
|
||||
"operation to copy from {:?} to {:?} cancelled",
|
||||
self.from,
|
||||
self.to
|
||||
);
|
||||
_ = futures::future::join(from_file.close(), to_file.close()).await;
|
||||
return Err(OperationError::from_state(state, &ctx.controller).into());
|
||||
}
|
||||
}
|
||||
|
||||
buf_in = buf_out;
|
||||
}
|
||||
|
||||
ctx.buf = buf_in;
|
||||
|
||||
if let Some(metadata) = metadata.as_ref() {
|
||||
let mut times = fs::FileTimes::new();
|
||||
if let Ok(time) = metadata.modified() {
|
||||
times = times.set_modified(time);
|
||||
}
|
||||
if let Ok(time) = metadata.accessed() {
|
||||
times = times.set_accessed(time);
|
||||
}
|
||||
//TODO: upstream set_times implementation to compio?
|
||||
let op = AsyncifyFd::new(to_file.to_shared_fd(), move |file: &std::fs::File| {
|
||||
BufResult(file.set_times(times).map(|_| 0), ())
|
||||
});
|
||||
match compio::runtime::submit(op).await.0.map(|_| ()) {
|
||||
Ok(()) => {
|
||||
tracing::info!("set times for {} to {:?}", self.to.display(), times);
|
||||
}
|
||||
Err(why) => {
|
||||
if !matches!(why.kind(), std::io::ErrorKind::Unsupported) {
|
||||
tracing::error!(?why, "failed to set times for {}", self.to.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = to_file.close().await;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Fallback mechanism in the event that unsupported I/O error errors occur.
|
||||
/// Fixes unsupported errors when copying large files over MTP.
|
||||
/// TODO: Find what Gio.File does to work around this.
|
||||
#[cfg(feature = "gvfs")]
|
||||
async fn gio_file_copy(
|
||||
&self,
|
||||
ctx: &mut Context,
|
||||
mut progress: Progress,
|
||||
) -> Result<(), GioCopyError> {
|
||||
_ = compio::fs::remove_file(&self.to).await;
|
||||
|
||||
let from = gio::File::for_path(&self.from);
|
||||
let to = gio::File::for_path(&self.to);
|
||||
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let (pause_tx, mut pause_rx) = tokio::sync::watch::channel(false);
|
||||
|
||||
let task = compio::runtime::spawn_blocking(move || {
|
||||
let glib_context = glib::MainContext::new();
|
||||
let glib_loop = glib::MainLoop::new(Some(&glib_context), false);
|
||||
glib_context.with_thread_default(move || {
|
||||
let glib_loop2 = glib_loop.clone();
|
||||
glib::MainContext::ref_thread_default().spawn_local(async move {
|
||||
// Create a future for copying the file with `gio::File`. This also creates a progress stream.
|
||||
let (gio_copy_fut, mut progress_stream) = from.copy_future(
|
||||
&to,
|
||||
gio::FileCopyFlags::OVERWRITE | gio::FileCopyFlags::ALL_METADATA,
|
||||
glib::Priority::LOW,
|
||||
);
|
||||
|
||||
let mut copy_fut = gio_copy_fut
|
||||
.map(|result| result.map_err(GioCopyError::GLib))
|
||||
.fuse();
|
||||
|
||||
let progress_fut = std::pin::pin!(async {
|
||||
while let Some((current_bytes, _)) = progress_stream.next().await {
|
||||
_ = progress_tx.send(current_bytes);
|
||||
}
|
||||
|
||||
drop(progress_tx);
|
||||
futures::future::pending::<()>().await;
|
||||
});
|
||||
|
||||
let mut progress_fut = progress_fut.fuse();
|
||||
let mut pause_rx2 = pause_rx.clone();
|
||||
|
||||
loop {
|
||||
let until_paused = std::pin::pin!(pause_rx.wait_for(|paused| *paused));
|
||||
futures::select! {
|
||||
_ = &mut progress_fut => {},
|
||||
|
||||
result = &mut copy_fut => {
|
||||
_ = tx.send(result.map(|_| ()));
|
||||
glib_loop2.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
_ = until_paused.fuse() => {
|
||||
_ = pause_rx2.wait_for(|paused| !*paused).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
glib_loop.run();
|
||||
})
|
||||
});
|
||||
|
||||
let mut last_progress_update = Instant::now();
|
||||
let mut task = task.fuse();
|
||||
let mut rx = rx.fuse();
|
||||
|
||||
loop {
|
||||
let until_paused = std::pin::pin!(ctx.controller.until_paused());
|
||||
futures::select! {
|
||||
value = progress_rx.recv().fuse() => {
|
||||
if let Some(current_bytes) = value {
|
||||
progress.current_bytes = current_bytes as u64;
|
||||
let current = Instant::now();
|
||||
if current.duration_since(last_progress_update).as_millis() > 49 {
|
||||
last_progress_update = current;
|
||||
(ctx.on_progress)(self, &progress);
|
||||
// Also check if the progress was cancelled.
|
||||
if let Err(state) = ctx.controller.check().await {
|
||||
tracing::warn!(
|
||||
"operation to copy from {:?} to {:?} cancelled",
|
||||
self.from,
|
||||
self.to
|
||||
);
|
||||
return Err::<(), GioCopyError>(GioCopyError::Controller(
|
||||
OperationError::from_state(state, &ctx.controller),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = rx => return result.unwrap(),
|
||||
|
||||
_ = task => (),
|
||||
|
||||
_ = until_paused.fuse() => {
|
||||
// Pauses an active copy while the controller state is paused.
|
||||
_ = pause_tx.send(true);
|
||||
ctx.controller.until_unpaused().await;
|
||||
_ = pause_tx.send(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
241
src/tab.rs
241
src/tab.rs
|
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "desktop")]
|
||||
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
|
||||
use cosmic::{
|
||||
Apply, Element, cosmic_theme,
|
||||
desktop::fde::{DesktopEntry, get_languages_from_env},
|
||||
font,
|
||||
Apply, Element, cosmic_theme, font,
|
||||
iced::core::{mouse::ScrollDelta, widget::tree},
|
||||
iced::{
|
||||
Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,
|
||||
|
|
@ -39,9 +39,10 @@ use icu::{
|
|||
use image::{DynamicImage, ImageReader};
|
||||
use jiff_icu::ConvertFrom;
|
||||
use mime_guess::{Mime, mime};
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::Cell,
|
||||
|
|
@ -52,7 +53,6 @@ use std::{
|
|||
fs::{self, File, Metadata},
|
||||
hash::Hash,
|
||||
io::{BufRead, BufReader},
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{self, Path, PathBuf},
|
||||
sync::{Arc, LazyLock, RwLock, atomic},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
|
|
@ -84,8 +84,8 @@ use crate::{
|
|||
operation::{Controller, OperationError},
|
||||
thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize},
|
||||
thumbnailer::thumbnailer,
|
||||
trash::{Trash, TrashExt},
|
||||
};
|
||||
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||
|
||||
pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
|
||||
pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
|
||||
|
|
@ -557,6 +557,12 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
|
|||
FsKind::Local
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
||||
let locales = get_languages_from_env();
|
||||
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
|
||||
|
|
@ -570,6 +576,12 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
|||
entry.name(&locales).map(|s| s.into_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
fn get_desktop_file_icon(path: &Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
fn get_desktop_file_icon(path: &Path) -> Option<String> {
|
||||
let entry = match DesktopEntry::from_path::<&str>(path, None) {
|
||||
Ok(ok) => ok,
|
||||
|
|
@ -593,6 +605,7 @@ fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
|
||||
let locales = get_languages_from_env();
|
||||
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
|
||||
|
|
@ -708,6 +721,9 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
|
|||
children_opt,
|
||||
},
|
||||
hidden,
|
||||
image_dimensions: (!remote && mime.type_() == mime::IMAGE)
|
||||
.then(|| image::image_dimensions(&path).ok())
|
||||
.flatten(),
|
||||
location_opt: Some(Location::Path(path)),
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
|
|
@ -843,6 +859,7 @@ pub fn item_from_entry(
|
|||
},
|
||||
hidden,
|
||||
location_opt: Some(Location::Path(path)),
|
||||
image_dimensions: None,
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -896,6 +913,9 @@ pub fn item_from_trash_entry(
|
|||
metadata: ItemMetadata::Trash { metadata, entry },
|
||||
hidden: false,
|
||||
location_opt: None,
|
||||
image_dimensions: (mime.type_() == mime::IMAGE)
|
||||
.then(|| image::image_dimensions(&original_path).ok())
|
||||
.flatten(),
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -1161,133 +1181,7 @@ pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
|
|||
}
|
||||
}
|
||||
SearchLocation::Trash => {
|
||||
trash_helpers::scan_search_trash(callback, ®ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This config statement is from trash::os_limited, inverted
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "android")
|
||||
)
|
||||
)))]
|
||||
mod trash_helpers {
|
||||
use super::*;
|
||||
|
||||
pub fn trash_entries() -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name("user-trash")
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
|
||||
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name("user-trash-symbolic")
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
|
||||
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
|
||||
log::warn!("viewing trash not supported on this platform");
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
|
||||
}
|
||||
|
||||
// This config statement is from trash::os_limited
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "android")
|
||||
)
|
||||
))]
|
||||
pub mod trash_helpers {
|
||||
use super::*;
|
||||
|
||||
pub fn trash_entries() -> usize {
|
||||
match trash::os_limited::list() {
|
||||
Ok(entries) => entries.len(),
|
||||
Err(_err) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trash_icon(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
||||
"user-trash"
|
||||
} else {
|
||||
"user-trash-full"
|
||||
})
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
|
||||
pub fn trash_icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name(if trash::os_limited::is_empty().unwrap_or(true) {
|
||||
"user-trash-symbolic"
|
||||
} else {
|
||||
"user-trash-full-symbolic"
|
||||
})
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
|
||||
pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
||||
let entries = match trash::os_limited::list() {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read trash items: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let mut items: Vec<_> = entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let metadata = trash::os_limited::metadata(&entry)
|
||||
.inspect_err(|err| {
|
||||
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||
})
|
||||
.ok()?;
|
||||
Some(item_from_trash_entry(entry, metadata, sizes))
|
||||
})
|
||||
.collect();
|
||||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
pub fn scan_search_trash<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||
let entries = match trash::os_limited::list() {
|
||||
Ok(entries) => entries,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read trash items: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
|
||||
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||
}) {
|
||||
let name = entry.name.to_string_lossy();
|
||||
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Trash::scan_search(callback, ®ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1425,15 +1319,15 @@ pub fn scan_desktop(
|
|||
let display_name = Item::display_name(&name);
|
||||
|
||||
let metadata = ItemMetadata::SimpleDir {
|
||||
entries: trash_helpers::trash_entries() as u64,
|
||||
entries: Trash::entries() as u64,
|
||||
};
|
||||
|
||||
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
|
||||
(
|
||||
"inode/directory".parse().unwrap(),
|
||||
trash_helpers::trash_icon(sizes.grid()),
|
||||
trash_helpers::trash_icon(sizes.list()),
|
||||
trash_helpers::trash_icon(sizes.list_condensed()),
|
||||
Trash::icon(sizes.grid()),
|
||||
Trash::icon(sizes.list()),
|
||||
Trash::icon(sizes.list_condensed()),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -1444,6 +1338,7 @@ pub fn scan_desktop(
|
|||
metadata,
|
||||
hidden: false,
|
||||
location_opt: Some(Location::Trash),
|
||||
image_dimensions: None,
|
||||
mime,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
|
|
@ -1673,7 +1568,7 @@ impl Location {
|
|||
// Search is done incrementally
|
||||
Vec::new()
|
||||
}
|
||||
Self::Trash => trash_helpers::scan_trash(sizes),
|
||||
Self::Trash => Trash::scan(sizes),
|
||||
Self::Recents => scan_recents(sizes),
|
||||
Self::Network(uri, _, _) => scan_network(uri, sizes),
|
||||
};
|
||||
|
|
@ -2319,6 +2214,7 @@ pub struct Item {
|
|||
pub hidden: bool,
|
||||
pub location_opt: Option<Location>,
|
||||
pub mime: Mime,
|
||||
pub image_dimensions: Option<(u32, u32)>,
|
||||
pub icon_handle_grid: widget::icon::Handle,
|
||||
pub icon_handle_list: widget::icon::Handle,
|
||||
pub icon_handle_list_condensed: widget::icon::Handle,
|
||||
|
|
@ -2533,7 +2429,7 @@ impl Item {
|
|||
|
||||
let mode = metadata.mode();
|
||||
|
||||
let user_name = get_user_by_uid(metadata.uid())
|
||||
let user_name = uzers::get_user_by_uid(metadata.uid())
|
||||
.and_then(|user| user.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default();
|
||||
let user_path = path.clone();
|
||||
|
|
@ -2556,7 +2452,7 @@ impl Item {
|
|||
)),
|
||||
);
|
||||
|
||||
let group_name = get_group_by_gid(metadata.gid())
|
||||
let group_name = uzers::get_group_by_gid(metadata.gid())
|
||||
.and_then(|group| group.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default();
|
||||
let group_path = path.clone();
|
||||
|
|
@ -4444,6 +4340,7 @@ impl Tab {
|
|||
for item in self.items_opt().map_or(Vec::new(), |items| {
|
||||
items.iter().filter(|item| item.selected).collect()
|
||||
}) {
|
||||
#[cfg(unix)]
|
||||
if let (Some(path), Some(mode)) = (
|
||||
item.path_opt(),
|
||||
item.file_metadata()
|
||||
|
|
@ -6461,20 +6358,23 @@ impl Tab {
|
|||
} else {
|
||||
total_size = total_size.saturating_add(metadata.len());
|
||||
}
|
||||
let mode = metadata.mode();
|
||||
user_name.insert(
|
||||
get_user_by_uid(metadata.uid())
|
||||
.and_then(|user| user.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER));
|
||||
group_name.insert(
|
||||
get_group_by_gid(metadata.gid())
|
||||
.and_then(|group| group.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP));
|
||||
mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mode = metadata.mode();
|
||||
user_name.insert(
|
||||
uzers::get_user_by_uid(metadata.uid())
|
||||
.and_then(|user| user.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER));
|
||||
group_name.insert(
|
||||
uzers::get_group_by_gid(metadata.gid())
|
||||
.and_then(|group| group.name().to_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
mode_group.insert(get_mode_part(mode, MODE_SHIFT_GROUP));
|
||||
mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect();
|
||||
|
|
@ -6713,13 +6613,13 @@ impl Tab {
|
|||
|
||||
// Determine effective memory budget based on image size
|
||||
let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {
|
||||
match image::image_dimensions(&path) {
|
||||
Ok((width, height)) => {
|
||||
match item.image_dimensions {
|
||||
Some((width, height)) => {
|
||||
let (_use_dedicated, eff_mb, eff_jobs) =
|
||||
should_use_dedicated_worker(width, height, max_mb, max_jobs);
|
||||
(eff_mb, eff_jobs)
|
||||
}
|
||||
Err(_) => (max_mb, max_jobs),
|
||||
None => (max_mb, max_jobs),
|
||||
}
|
||||
} else {
|
||||
(max_mb, max_jobs)
|
||||
|
|
@ -6762,6 +6662,10 @@ impl Tab {
|
|||
stream::channel(
|
||||
1,
|
||||
move |mut output: futures::channel::mpsc::Sender<_>| async move {
|
||||
while crate::operation::is_actively_writing_to(&path) {
|
||||
crate::operation::actively_writing_tick().await;
|
||||
}
|
||||
|
||||
let message = {
|
||||
let path = path.clone();
|
||||
|
||||
|
|
@ -6946,9 +6850,8 @@ impl Tab {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let output = Arc::new(tokio::sync::Mutex::new(output));
|
||||
let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true);
|
||||
{
|
||||
let output = output.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
scan_search(
|
||||
&search_location,
|
||||
|
|
@ -6976,14 +6879,7 @@ impl Tab {
|
|||
true
|
||||
} else {
|
||||
// Wake up update method
|
||||
futures::executor::block_on(async {
|
||||
output
|
||||
.lock()
|
||||
.await
|
||||
.send(Message::SearchReady(false))
|
||||
.await
|
||||
})
|
||||
.is_ok()
|
||||
watch_tx.send(false).is_ok()
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
|
|
@ -6996,13 +6892,16 @@ impl Tab {
|
|||
search_location,
|
||||
start.elapsed(),
|
||||
);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
while watch_rx.changed().await.is_ok() {
|
||||
let is_ready = *watch_rx.borrow_and_update();
|
||||
let _ = output.send(Message::SearchReady(is_ready)).await;
|
||||
}
|
||||
|
||||
// Send final ready
|
||||
let _ = output.lock().await.send(Message::SearchReady(true)).await;
|
||||
let _ = output.send(Message::SearchReady(true)).await;
|
||||
|
||||
std::future::pending().await
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use image::DynamicImage;
|
||||
use md5::{Digest, Md5};
|
||||
use rustc_hash::FxHashMap;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
error::Error,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
time::UNIX_EPOCH,
|
||||
|
|
@ -93,6 +94,7 @@ impl ThumbnailCacher {
|
|||
}
|
||||
|
||||
pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box<dyn Error>> {
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?;
|
||||
self.update_thumbnail_text_metadata(temp_file.path())?;
|
||||
fs::rename(temp_file.path(), &self.thumbnail_path)?;
|
||||
|
|
@ -127,6 +129,7 @@ impl ThumbnailCacher {
|
|||
pub fn create_fail_marker(&self) -> Result<(), Box<dyn Error>> {
|
||||
if let Some(dir) = self.thumbnail_fail_marker_path.parent() {
|
||||
fs::create_dir_all(dir)?;
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
use cosmic::desktop::fde::GenericEntry;
|
||||
use mime_guess::Mime;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
|
|
|||
145
src/trash.rs
Normal file
145
src/trash.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
use cosmic::widget;
|
||||
use regex::Regex;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
config::IconSizes,
|
||||
tab::{Item, SearchItem},
|
||||
};
|
||||
|
||||
pub trait TrashExt {
|
||||
fn is_empty() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entries() -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
|
||||
Err(trash::Error::Unknown {
|
||||
description: "reading trash folders not supported on this platform".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn scan(_sizes: IconSizes) -> Vec<Item> {
|
||||
log::warn!("viewing trash not supported on this platform");
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {}
|
||||
|
||||
fn icon(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name(if Self::is_empty() {
|
||||
"user-trash"
|
||||
} else {
|
||||
"user-trash-full"
|
||||
})
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
|
||||
fn icon_symbolic(icon_size: u16) -> widget::icon::Handle {
|
||||
widget::icon::from_name(if Self::is_empty() {
|
||||
"user-trash-symbolic"
|
||||
} else {
|
||||
"user-trash-full-symbolic"
|
||||
})
|
||||
.size(icon_size)
|
||||
.handle()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Trash;
|
||||
|
||||
// This config statement is from trash::os_limited
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "android")
|
||||
)
|
||||
))]
|
||||
impl TrashExt for Trash {
|
||||
fn is_empty() -> bool {
|
||||
trash::os_limited::is_empty().unwrap_or(true)
|
||||
}
|
||||
|
||||
fn entries() -> usize {
|
||||
match trash::os_limited::list() {
|
||||
Ok(entries) => entries.len(),
|
||||
Err(_err) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Not available on Windows only
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn folders() -> Result<HashSet<PathBuf>, trash::Error> {
|
||||
trash::os_limited::trash_folders()
|
||||
}
|
||||
|
||||
fn scan(sizes: IconSizes) -> Vec<Item> {
|
||||
use crate::{localize::LANGUAGE_SORTER, tab::item_from_trash_entry};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let entries = match trash::os_limited::list() {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read trash items: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let mut items: Vec<_> = entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let metadata = trash::os_limited::metadata(&entry)
|
||||
.inspect_err(|err| {
|
||||
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||
})
|
||||
.ok()?;
|
||||
Some(item_from_trash_entry(entry, metadata, sizes))
|
||||
})
|
||||
.collect();
|
||||
items.sort_by(|a, b| match (a.metadata.is_dir(), b.metadata.is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => LANGUAGE_SORTER.compare(&a.display_name, &b.display_name),
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
fn scan_search<F: Fn(SearchItem) -> bool + Sync>(callback: F, regex: &Regex) {
|
||||
let entries = match trash::os_limited::list() {
|
||||
Ok(entries) => entries,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read trash items: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
if let Ok(metadata) = trash::os_limited::metadata(&entry).inspect_err(|err| {
|
||||
log::warn!("failed to get metadata for trash item {entry:?}: {err}")
|
||||
}) {
|
||||
let name = entry.name.to_string_lossy();
|
||||
if regex.is_match(&name) && !callback(SearchItem::Trash(entry, metadata)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This config statement is from trash::os_limited, inverted
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
unix,
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "ios"),
|
||||
not(target_os = "android")
|
||||
)
|
||||
)))]
|
||||
impl TrashExt for Trash {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue