Compare commits

..

No commits in common. "9bcfe7a1f58580c713e21703e5c2fb2acd397cda" and "91243b99b556586c16c7c9b89872d4784f93c654" have entirely different histories.

42 changed files with 774 additions and 1864 deletions

426
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-files" name = "cosmic-files"
version = "1.0.11" version = "1.0.9"
authors = ["Jeremy Soller <jeremy@system76.com>"] authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
@ -14,6 +14,7 @@ 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 } 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 } cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0" dirs = "6.0.0"
env_logger = "0.11"
gio = { version = "0.21", optional = true } gio = { version = "0.21", optional = true }
glib = { version = "0.21", optional = true } glib = { version = "0.21", optional = true }
glob = "0.3" glob = "0.3"
@ -55,16 +56,12 @@ rust-embed = "8"
slotmap = "1.1.1" slotmap = "1.1.1"
recently-used-xbel = "1.2.0" recently-used-xbel = "1.2.0"
zip = "8" zip = "8"
uzers = "0.12.2"
md-5 = "0.10.6" md-5 = "0.10.6"
png = "0.18" png = "0.18"
jxl-oxide = { version = "0.12.5", features = ["image"] } jxl-oxide = { version = "0.12.5", features = ["image"] }
num_cpus = "1.17.0" num_cpus = "1.17.0"
filetime = "0.2" 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. # Completion-based IO runtime to enable io_uring / IOCP file IO support.
[dependencies.compio] [dependencies.compio]
@ -80,6 +77,7 @@ features = [
"about", "about",
"advanced-shaping", "advanced-shaping",
"autosize", "autosize",
"desktop",
"multi-window", "multi-window",
"tokio", "tokio",
"winit", "winit",
@ -111,7 +109,7 @@ default = [
"wgpu", "wgpu",
] ]
dbus-config = ["libcosmic/dbus-config"] dbus-config = ["libcosmic/dbus-config"]
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"] desktop = ["dep:cosmic-mime-apps", "dep:xdg"]
desktop-applet = [] desktop-applet = []
gvfs = ["dep:gio", "dep:glib"] gvfs = ["dep:gio", "dep:glib"]
io-uring = ["compio/io-uring"] io-uring = ["compio/io-uring"]
@ -129,7 +127,6 @@ debug = true
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
fork = "0.7" fork = "0.7"
uzers = "0.12.2"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.18" procfs = "0.18"
@ -147,12 +144,13 @@ tokio = { version = "1", features = ["rt", "macros"] }
# [patch.'https://github.com/pop-os/cosmic-text'] # [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { path = "../cosmic-text" } # cosmic-text = { path = "../cosmic-text" }
[patch.'https://github.com/pop-os/libcosmic'] # [patch.'https://github.com/pop-os/libcosmic']
libcosmic = { path = "../libcosmic" } # libcosmic = { path = "../libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" } # cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" } # cosmic-theme = { path = "../libcosmic/cosmic-theme" }
iced_futures = { path = "../libcosmic/iced/futures" } # libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
iced_winit = { path = "../libcosmic/iced/winit" } # 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/smithay-clipboard'] # [patch.'https://github.com/pop-os/smithay-clipboard']

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-files-applet" name = "cosmic-files-applet"
version = "1.0.11" version = "1.0.9"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

6
debian/changelog vendored
View file

@ -1,9 +1,3 @@
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 cosmic-files (1.0.9) noble; urgency=medium
* Epoch 1.0.9 version update * Epoch 1.0.9 version update

View file

@ -9,25 +9,9 @@ use cosmic_files::dialog::{
Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind, Dialog, DialogChoice, DialogChoiceOption, DialogFilter, DialogFilterPattern, DialogKind,
DialogMessage, DialogResult, DialogSettings, DialogMessage, DialogResult, DialogSettings,
}; };
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let log_format = tracing_subscriber::fmt::format() env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
.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(); let settings = Settings::default();
app::run::<App>(settings, ())?; app::run::<App>(settings, ())?;

View file

@ -22,10 +22,10 @@ empty-trash-warning = سيتم حذف العناصر الموجودة في مج
## New File/Folder Dialog ## New File/Folder Dialog
create-new-file = أنشئ ملف جديد create-new-file = إنشاء ملف جديد
create-new-folder = أنشئ مجلَّد جديد create-new-folder = إنشاء مجلد جديد
file-name = اسم الملف file-name = اسم الملف
folder-name = اسم المجلَّد folder-name = اسم المجلد
file-already-exists = يوجد ملف بهذا الاسم بالفعل file-already-exists = يوجد ملف بهذا الاسم بالفعل
folder-already-exists = يوجد مجلد بهذا الاسم بالفعل folder-already-exists = يوجد مجلد بهذا الاسم بالفعل
name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية name-hidden = الاسماء التي تبدأ بنقطة «.» ستكون مخفية
@ -34,10 +34,10 @@ name-no-slashes = لا يمكن أن يحتوي الاسم على شرطات م
## Open/Save Dialog ## Open/Save Dialog
cancel = ألغِ cancel = إلغاء
open = افتح open = فتح
open-file = افتح ملف open-file = فتح ملف
open-folder = افتح مجلَّد open-folder = افتح مجلد
open-in-new-tab = افتح في لسان جديد open-in-new-tab = افتح في لسان جديد
open-in-new-window = افتح في نافذة جديدة open-in-new-window = افتح في نافذة جديدة
open-multiple-files = افتح عدة ملفات open-multiple-files = افتح عدة ملفات
@ -150,12 +150,12 @@ trashed-on = مهمل
details = التفاصيل details = التفاصيل
pause = ألبث pause = ألبث
resume = استئناف resume = استئناف
create-archive = أنشئ أرشيف create-archive = إنشاء أرشيف
extract-password-required = كلمة السر مطلوبة extract-password-required = كلمة السر مطلوبة
extract-to = استخرِج إلى... extract-to = استخرِج إلى...
extract-to-title = استخرِج إلى مجلّد extract-to-title = استخرِج إلى مجلّد
mount-error = تعذر الوصول إلى القرص mount-error = تعذر الوصول إلى القرص
create = أنشئ create = إنشاء
open-item-location = افتح مكان العنصر open-item-location = افتح مكان العنصر
set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟ set-executable-and-launch-description = أتريد تعيين «{ $name }» كقابل للتنفيذ وتشغيله؟
favorite-path-error-description = favorite-path-error-description =
@ -259,7 +259,7 @@ related-apps = تطبيقات ذات صلة
selected-items = العناصر { $items } المحدّدة selected-items = العناصر { $items } المحدّدة
permanently-delete-question = احذف نهائيًا؟ permanently-delete-question = احذف نهائيًا؟
delete = احذف delete = احذف
permanently-delete-warning = سيُحذف { $target } نهائيًا. لا يمكن التراجع عن هذا الإجراء. permanently-delete-warning = هل أنت متأكد من أنك تريد حذف { $target } نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.
replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه. replace-warning-operation = أتريد استبداله؟ استبداله سيكتب فوق محتواه.
original-file = الملف الأصلي original-file = الملف الأصلي
replace-with = استبدل بـ replace-with = استبدل بـ
@ -371,4 +371,3 @@ move-to = انقل إلى...
show-recents = مجلد الحديثة في الشريط الجانبي show-recents = مجلد الحديثة في الشريط الجانبي
clear-recents-history = امحُ التأريخ الحديث clear-recents-history = امحُ التأريخ الحديث
copy-path = انسخ المسار copy-path = انسخ المسار
mixed = مختلط

View file

@ -176,7 +176,7 @@ set-executable-and-launch-description = Chcete povolit spouštění souboru „{
set-and-launch = Povolit a spustit set-and-launch = Povolit a spustit
open-with = Otevřít pomocí open-with = Otevřít pomocí
other = Ostatní other = Ostatní
none = Žádné none = Žádný
icon-size-and-spacing = Velikost a rozestupy ikon icon-size-and-spacing = Velikost a rozestupy ikon
grid-spacing = Rozestupy mřížky grid-spacing = Rozestupy mřížky
deleting = deleting =
@ -417,4 +417,3 @@ move-to-title = Vyberte cíl přesunutí
show-recents = Složka „Nedávné“ v postranním panelu show-recents = Složka „Nedávné“ v postranním panelu
copy-path = Kopírovat cestu copy-path = Kopírovat cestu
clear-recents-history = Vymazat historii „Nedávné“ clear-recents-history = Vymazat historii „Nedávné“
mixed = Různé

View file

@ -330,7 +330,7 @@ light = Hell
type-to-search = Zum Suchen tippen type-to-search = Zum Suchen tippen
type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner
type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein type-to-search-enter-path = Gibt den Pfad zu einem Verzeichnis oder einer Datei ein
# Kontextmenü # Kontextmenü
add-to-sidebar = Zur Seitenleiste hinzufügen add-to-sidebar = Zur Seitenleiste hinzufügen
compress = Komprimieren... compress = Komprimieren...
@ -376,9 +376,9 @@ select-all = Alles auswählen
## Ansicht ## Ansicht
zoom-in = Hineinzoomen zoom-in = Vergrößern
default-size = Standardgröße default-size = Standardgröße
zoom-out = Herauszoomen zoom-out = Verkleinern
view = Ansicht view = Ansicht
grid-view = Rasteransicht grid-view = Rasteransicht
list-view = Listenansicht list-view = Listenansicht
@ -390,7 +390,7 @@ menu-about = Über COSMIC Dateien...
## Sortieren ## Sortieren
sort = Sortierung sort = Sortieren
sort-a-z = A-Z sort-a-z = A-Z
sort-z-a = Z-A sort-z-a = Z-A
sort-newest-first = Neueste zuerst sort-newest-first = Neueste zuerst
@ -424,8 +424,3 @@ clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren
comment = Dateimanager für den COSMIC Desktop comment = Dateimanager für den COSMIC Desktop
keywords = Ordner;Manager; keywords = Ordner;Manager;
move-to-button-label = Verschieben 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

View file

@ -5,7 +5,7 @@ no-results = No se encontraron resultados
filesystem = Sistema de archivos filesystem = Sistema de archivos
home = Inicio home = Inicio
networks = Redes 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 trash = Papelera
recents = Recientes recents = Recientes
undo = Deshacer undo = Deshacer

View file

@ -5,7 +5,7 @@ no-results = Ei tuloksia
filesystem = Tiedostojärjestelmä filesystem = Tiedostojärjestelmä
home = Koti home = Koti
networks = Verkot networks = Verkot
notification-in-progress = Tiedostotoimintoja käynnissä notification-in-progress = Tiedostotoimintoja käynnissä.
trash = Roskakori trash = Roskakori
recents = Viimeaikaiset recents = Viimeaikaiset
undo = Kumoa undo = Kumoa
@ -16,7 +16,7 @@ today = Tänään
desktop-view-options = Työpöytänäkymän asetukset… desktop-view-options = Työpöytänäkymän asetukset…
show-on-desktop = Näytä työpöydällä show-on-desktop = Näytä työpöydällä
desktop-folder-content = Työpöytäkansion sisältö desktop-folder-content = Työpöytäkansion sisältö
mounted-drives = Liitetyt asemat mounted-drives = Tiedostojärjestelmään liitetyt kovalevyt
trash-folder-icon = Roskakorikansion kuvake trash-folder-icon = Roskakorikansion kuvake
icon-size-and-spacing = Kuvakkeen koko ja välistys icon-size-and-spacing = Kuvakkeen koko ja välistys
icon-size = Kuvakkeen koko icon-size = Kuvakkeen koko
@ -38,7 +38,7 @@ create-archive = Luo arkisto
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = Tyhjennä roskakori empty-trash = Tyhjennä roskakori
empty-trash-warning = Roskakorikansion kohteet poistetaan pysyvästi empty-trash-warning = Haluatko varmasti tyhjentää koko roskakorin pysyvästi?
## Mount Error Dialog ## Mount Error Dialog
@ -73,8 +73,8 @@ save-file = Tallenna tiedosto
## Open With Dialog ## Open With Dialog
open-with-title = Miten haluat avata kohteen "{ $name }"? open-with-title = Kuinka haluat avata kohteen "{ $name }"?
browse-store = Selaa { $store }a browse-store = Selaa { $store }
## Rename Dialog ## 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 = Haluatko korvata sen tallentamallasi kohteella? Korvaaminen ylikirjoittaa kohteen sisällön.
replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön. replace-warning-operation = Haluatko korvata sen? Korvaaminen ylikirjoittaa sen sisällön.
original-file = Alkuperäinen tiedosto original-file = Alkuperäinen tiedosto
replace-with = Korvaa käyttäen replace-with = Korvaa kohteella
apply-to-all = Toteuta kaikkiin apply-to-all = Sovella kaikkiin
keep-both = Pidä molemmat keep-both = Pidä molemmat
skip = Ohita skip = Ohita
## Set as Executable and Launch Dialog ## Set as Executable and Launch Dialog
set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä set-executable-and-launch = Aseta käynnistettäväksi ja käynnistä
set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettäväksi ja käynnistää sen? set-executable-and-launch-description = Haluatko asettaa kohteen "{ $name }" käynnistettväksi ja käynnistää sen?
set-and-launch = Aseta ja käynnistä set-and-launch = Aseta ja käynnistä
## Metadata Dialog ## Metadata Dialog
@ -116,9 +116,9 @@ other = Muut
add-network-drive = Lisää verkkolevy add-network-drive = Lisää verkkolevy
connect = Yhdistä connect = Yhdistä
connect-anonymously = Yhdistä nimettömästi connect-anonymously = Yhdistä nimettömästi
connecting = Yhdistetään… connecting = Yhdistää…
domain = Verkkotunnus domain = Verkkotunnus
enter-server-address = Kirjoita palvelimen osoite enter-server-address = Syötä palvelimen osoite
network-drive-description = network-drive-description =
Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen. Palvelinosoitteet sisältävät protokollaetuliitteen sekä osoitteen.
Esimerkkejä: ssh://192.168.0.1, ftp://[2001:db8::1] 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 ### Make sure to keep the comma which separates the columns
network-drive-schemes = network-drive-schemes =
Saatavilla olevat yhteyskäytännöt,Etuliite Saatavissa olevat protokollat,Etuliite
AppleTalk,afp:// AppleTalk,afp://
File Transfer Protocol,ftp:// tai ftps:// File Transfer Protocol,ftp:// or ftps://
Network File System,nfs:// Network File System,nfs://
Server Message Block,smb:// Server Message Block,smb://
SSH File Transfer Protocol,sftp:// tai ssh:// SSH File Transfer Protocol,sftp:// or ssh://
WebDav,dav:// tai davs:// WebDav,dav:// or davs://
network-drive-error = Verkkolevy ei saatavilla network-drive-error = Verkkolevy saavuttamattomissa
password = Salasana password = Salasana
remember-password = Muista salasana remember-password = Muista salasana
try-again = Yritä uudelleen try-again = Yritä uudelleen
username = Käyttäjätunnus username = Käyttäjänimi
## Operations ## Operations
edit-history = Muokkaa historiaa edit-history = Muokkaa historiaa
history = Historia history = Historia
no-history = Historia on tyhjä. no-history = Historia on tyhjä.
pending = Jonossa pending = Odottaa käsittelyä
failed = Epäonnistuneet failed = Epäonnistui
complete = Valmiit complete = Valmis
compressing = compressing =
Pakataan { $items } { $items -> Tiivistetään { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteita
} sijainnista "{ $from }" arkistoon "{ $to }" ({ $progress })… } lähteestä "{ $from }" arkistoon "{ $to }"
compressed = compressed =
Pakattu { $items } { $items -> Tiivistetty { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} sijainnista "{ $from }" arkistoon "{ $to }" } lähteestä "{ $from }" arkistoon "{ $to }"
copy_noun = Kopio copy_noun = Kopio
creating = Luodaan "{ $name }" kohteen "{ $parent }" alle creating = Luodaan kohdetta "{ $name }" kohteen "{ $parent }" alle
created = Luotu "{ $name }" kohteen "{ $parent }" alle created = Luotu kohde "{ $name }" kohteen "{ $parent }" alle
copying = copying =
Kopioidaan { $items } { $items -> Kopioidaan { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteita
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… } lähteestä "{ $from }" kohteeseen "{ $to }"
copied = copied =
Kopioitu { $items } { $items -> Kopioitu { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} sijainnista "{ $from }" kohteeseen "{ $to }" } lähteestä "{ $from }" kohteeseen "{ $to }"
emptying-trash = Tyhjennetään { trash } ({ $progress })… emptying-trash = Tyhjennetään { trash }
emptied-trash = Tyhjennetty { trash } emptied-trash = Tyhjennetty { trash }
extracting = extracting =
Puretaan { $items } { $items -> Puretaan { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} arkistosta "{ $from }" kohteeseen "{ $to }" ({ $progress })… } arkistosta "{ $from }" kohteeseen "{ $to }"
extracted = extracted =
Purettu { $items } { $items -> Purettu { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} arkistosta "{ $from }" kohteeseen "{ $to }" } arkistosta "{ $from }" kohteeseen "{ $to }"
setting-executable-and-launching = Asetetaan "{ $name }" käynnistettäväksi ja käynnistetään 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 set-executable-and-launched = Asetettu "{ $name }" käynnistettäväksi ja käynnistetty
moving = moving =
Siirretään { $items } { $items -> Siirretään { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} sijainnista "{ $from }" kohteeseen "{ $to }" ({ $progress })… } lähteestä "{ $from }" kohteeseen "{ $to }"
moved = moved =
Siirretty { $items } { $items -> Siirretty { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} sijainnista "{ $from }" kohteeseen "{ $to }" } lähteestä "{ $from }" kohteeseen "{ $to }"
renaming = Nimetään kohde "{ $from }" muotoon "{ $to }" renaming = Nimetään kohde "{ $from }" muotoon "{ $to }"
renamed = Nimetty kohde "{ $from }" muotoon "{ $to }" renamed = Nimetty kohde "{ $from }" muotoon "{ $to }"
restoring = restoring =
Palautetaan { $items } { $items -> Palautetaan { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} roskakorista ({ $progress })… } { trash }sta
restored = restored =
Palautettu { $items } { $items -> Palautettu { $items } { $items ->
[one] kohde [one] kohde
*[other] kohdetta *[other] kohteet
} roskakorista } { trash }sta
unknown-folder = tuntematon kansio unknown-folder = Tuntematon kansio
## Open with ## Open with
menu-open-with = Avaa sovelluksella… menu-open-with = Avaa ohjelmalla…
default-app = { $name } (oletus) default-app = { $name } (oletus)
## Show details ## Show details
@ -225,14 +225,14 @@ settings = Asetukset
appearance = Ulkoasu appearance = Ulkoasu
theme = Teema theme = Teema
match-desktop = Sovita työpöytään match-desktop = Sovita yhteen työpöydän kanssa
dark = Tumma dark = Tumma
light = Vaalea light = Vaalea
# Context menu # Context menu
add-to-sidebar = Lisää sivupalkkiin add-to-sidebar = Lisää sivupalkkiin
compress = Pakkaa… compress = Pakkaa
extract-here = Pura extract-here = Pura
new-file = Uusi tiedosto… new-file = Uusi tiedosto…
new-folder = Uusi kansio… new-folder = Uusi kansio…
@ -241,9 +241,9 @@ move-to-trash = Siirrä roskakoriin
restore-from-trash = Palauta roskakorista restore-from-trash = Palauta roskakorista
remove-from-sidebar = Poista sivupalkista remove-from-sidebar = Poista sivupalkista
sort-by-name = Järjestä nimen mukaan sort-by-name = Järjestä nimen mukaan
sort-by-modified = Järjestä muokkausajan mukaan sort-by-modified = Järjestä muokkauspäivämäärän mukaan
sort-by-size = Järjestä koon mukaan sort-by-size = Järjestä koon mukaan
sort-by-trashed = Järjestä poistamisajan mukaan sort-by-trashed = Järjestä poistamispäivämäärän mukaan
## Desktop ## Desktop
@ -281,9 +281,9 @@ grid-view = Ruudukkonäkymä
list-view = Listanäkymä list-view = Listanäkymä
show-hidden-files = Näytä piilotetut tiedostot show-hidden-files = Näytä piilotetut tiedostot
list-directories-first = Näytä kansiot ensin list-directories-first = Näytä kansiot ensin
gallery-preview = Gallerian esikatselu gallery-preview = Gallerian esinäkymä
menu-settings = Asetukset… menu-settings = Asetukset…
menu-about = Tietoa COSMICin tiedostonhallinnasta… menu-about = Tietoa COSMIC Tiedostoista…
## Sort ## Sort
@ -318,90 +318,3 @@ move-to-button-label = Siirrä
clear-recents-history = Tyhjennä viimeaikaisten historia clear-recents-history = Tyhjennä viimeaikaisten historia
copy-path = Kopioi polku copy-path = Kopioi polku
dismiss = Hylkää viesti 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

View file

@ -436,4 +436,3 @@ keywords = Dossier;Gestionnaire;
show-recents = Dossier Récents dans la barre latérale show-recents = Dossier Récents dans la barre latérale
copy-path = Copier le chemin copy-path = Copier le chemin
clear-recents-history = Effacer l'historique des Récents clear-recents-history = Effacer l'historique des Récents
mixed = Mixte

View file

@ -433,4 +433,3 @@ keywords = Fillteán;Bainisteoir;
show-recents = Fillteán le déanaí sa bharra taoibh show-recents = Fillteán le déanaí sa bharra taoibh
clear-recents-history = Glan stair na n-earraí le déanaí clear-recents-history = Glan stair na n-earraí le déanaí
copy-path = Cóipeáil an chosán copy-path = Cóipeáil an chosán
mixed = Measctha

View file

@ -436,4 +436,3 @@ move-to = Áthelyezés ide…
show-recents = Legutóbbiak mappa megjelenítése az oldalsávban show-recents = Legutóbbiak mappa megjelenítése az oldalsávban
copy-path = Útvonal másolása copy-path = Útvonal másolása
clear-recents-history = Legutóbbiak előzményének törlése clear-recents-history = Legutóbbiak előzményének törlése
mixed = Vegyes

View file

@ -318,4 +318,3 @@ comment = Pengelola berkas untuk desktop COSMIC
show-recents = Map terbaru di bilah sisi show-recents = Map terbaru di bilah sisi
clear-recents-history = Bersihkan riwayat Terbaru clear-recents-history = Bersihkan riwayat Terbaru
copy-path = Salin jalur copy-path = Salin jalur
mixed = Bercampur

View file

@ -1,322 +1 @@
change-wallpaper = Beddel aɣrab n ugdil… 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

View file

@ -256,7 +256,7 @@ type-to-search-recursive = Ағымдағы бума мен барлық ішк
type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді
type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды
add-to-sidebar = Бүйірлік панельге қосу add-to-sidebar = Бүйірлік панельге қосу
compress = Сығу... compress = Сығу
delete-permanently = Біржолата өшіру delete-permanently = Біржолата өшіру
eject = Шығару eject = Шығару
extract-here = Тарқату extract-here = Тарқату
@ -318,4 +318,3 @@ keywords = Folder;Manager;Бума;Басқарушы;
show-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы show-recents = Бүйір панеліндегі «Жуырдағы құжаттар» бумасы
clear-recents-history = Жуырдағылар тарихын өшіру clear-recents-history = Жуырдағылар тарихын өшіру
copy-path = Орналасқан жолын көшіру copy-path = Орналасқан жолын көшіру
mixed = Аралас

View file

@ -110,7 +110,7 @@ dismiss = 메시지 무시
copy_noun = 복사 copy_noun = 복사
progress = { $percent }% progress = { $percent }%
related-apps = 관련 앱 related-apps = 관련 앱
compress = 압축... compress = 압축
network-drive-error = 네트워크 드라이브에 접근할 수 없음 network-drive-error = 네트워크 드라이브에 접근할 수 없음
icon-size-and-spacing = 아이콘 크기 및 간격 icon-size-and-spacing = 아이콘 크기 및 간격
password = 암호 password = 암호
@ -272,8 +272,3 @@ network-drive-schemes =
WebDAV,dav:// 또는 davs:// WebDAV,dav:// 또는 davs://
type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다 type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다
comment = COSMIC 데스크톱용 위한 파일 관리자 comment = COSMIC 데스크톱용 위한 파일 관리자
keywords = 폴더;관리자;
copy-to-button-label = 복사
move-to-button-label = 이동
clear-recents-history = 최근 기록 비우기
copy-path = 복사 경로

View file

@ -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-recursive = Paieška dabartiniame aplanke ir jo poaplankiuose
type-to-search-enter-path = Įvedamas aplanko ar failo kelias type-to-search-enter-path = Įvedamas aplanko ar failo kelias
add-to-sidebar = Pridėti į šonjuostę add-to-sidebar = Pridėti į šonjuostę
compress = Suspausti... compress = Suspausti
delete-permanently = Ištrinti visam laikui delete-permanently = Ištrinti visam laikui
eject = Išstumti eject = Išstumti
extract-here = Išskleisti extract-here = Išskleisti

View file

@ -32,7 +32,7 @@ pause = ਵਿਰਾਮ
resume = ਮੁੜ-ਚਾਲੂ resume = ਮੁੜ-ਚਾਲੂ
create-archive = ਅਕਾਇਵ ਬਣਾਓ create-archive = ਅਕਾਇਵ ਬਣਾਓ
extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ extract-password-required = ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ
extract-to = ਖਿਲਾਰੋ... extract-to = Extract To...
extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ extract-to-title = ਫੋਲਡਰ ਵਿੱਚ ਖਿਲਾਰੋ
empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ empty-trash = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰੋ
empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ? empty-trash-title = ਰੱਦੀ ਨੂੰ ਖਾਲੀ ਕਰਨਾ ਹੈ?
@ -113,7 +113,7 @@ replace-title = "{ $filename }" ਪਹਿਲਾਂ ਹੀ ਇਸ ਟਿਕਾਣ
favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ favorite-path-error = ਡਾਇਰੈਕਟਰੀ ਖੋਲ੍ਹਣ ਦੌਰਾਨ ਗਲਤੀ
add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ add-network-drive = ਨੈੱਟਵਰਕ ਡਰਾਇਵ ਜੋੜੋ
connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ connect-anonymously = ਅਣਪਛਾਤੇ ਵਜੋਂ ਕਨੈਕਟ ਕਰੋ
connecting = ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ... connecting = Connecting...
domain = ਡੋਮੇਨ domain = ਡੋਮੇਨ
enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ enter-server-address = ਸਰਵਰ ਦਾ ਸਿਰਨਾਵਾਂ ਦਿਓ
password = ਪਾਸਵਰਡ password = ਪਾਸਵਰਡ
@ -138,9 +138,9 @@ sort-by-modified = ਸੋਧ ਨਾਲ ਲੜੀਬੱਧ
sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ sort-by-size = ਆਕਾਰ ਨਾਲ ਲੜੀਬੱਧ
sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ sort-by-trashed = ਹਟਾਉਣ ਸਮੇਂ ਨਾਲ ਲੜੀਬੱਧ
remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ remove-from-recents = ਸੱਜਰਿਆਂ ਵਿੱਚੋਂ ਹਟਾਓ
change-wallpaper = ਵਾਲਪੇਪਰ ਨੂੰ ਬਦਲੋ... change-wallpaper = Change wallpaper...
desktop-appearance = ਡੈਸਕਟਾਪ ਦੀ ਦਿੱਖ... desktop-appearance = Desktop appearance...
display-settings = ਡਿਸਪਲੇਅ ਸੈਟਿੰਗਾਂ... display-settings = Display settings...
file = ਫ਼ਾਇਲ file = ਫ਼ਾਇਲ
new-tab = ਨਵੀਂ ਟੈਬ new-tab = ਨਵੀਂ ਟੈਬ
new-window = ਨਵੀਂ ਵਿੰਡੋ new-window = ਨਵੀਂ ਵਿੰਡੋ
@ -195,11 +195,11 @@ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ
type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ
add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ
compress = ਕੰਪਰੈਸ compress = ਕੰਪਰੈਸ
copy-to = ਇੱਥੇ ਕਾਪੀ ਕਰੋ... copy-to = Copy to...
delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ delete-permanently = ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਓ
eject = ਬਾਹਰ eject = ਬਾਹਰ
extract-here = ਖਿਲਾਰੋ extract-here = ਖਿਲਾਰੋ
move-to = ਇੱਥੇ ਭੇਜੋ... move-to = Move to...
remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ remove-from-sidebar = ਬਾਹੀ ਵਿੱਚੋਂ ਹਟਾਓ
reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ reload-folder = ਫੋਲਡਰ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰੋ
list-view = ਸੂਚੀ ਝਲਕ list-view = ਸੂਚੀ ਝਲਕ

View file

@ -437,4 +437,3 @@ move-to = Przenieś do…
show-recents = Ostatnie katalogi w panelu bocznym show-recents = Ostatnie katalogi w panelu bocznym
clear-recents-history = Wyczyść bierzącą historię clear-recents-history = Wyczyść bierzącą historię
copy-path = Skopiuj ścieżkę copy-path = Skopiuj ścieżkę
mixed = Mieszane

View file

@ -424,8 +424,8 @@ sort-largest-to-smallest = Do maior para o menor
empty-trash-title = Esvaziar a lixeira? empty-trash-title = Esvaziar a lixeira?
type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente type-to-search-select = Seleciona o primeiro arquivo ou pasta correspondente
pasted-image = Imagem colada pasted-image = Imagem colada
pasted-text = Texto colado pasted-text = Texto copiado
pasted-video = Vídeo colado pasted-video = Vídeo copiado
copy-to-title = Selecione o destino da cópia copy-to-title = Selecione o destino da cópia
copy-to-button-label = Copiar copy-to-button-label = Copiar
move-to-title = Selecione o destino da movimentação move-to-title = Selecione o destino da movimentação
@ -433,7 +433,6 @@ move-to-button-label = Mover
copy-to = Copiar para... copy-to = Copiar para...
move-to = Mover para... move-to = Mover para...
keywords = Pasta;Gerenciador; keywords = Pasta;Gerenciador;
show-recents = Pasta de recentes na barra lateral show-recents = Pasta Recentes na barra lateral
clear-recents-history = Limpar histórico de recentes clear-recents-history = Limpar histórico de recentes
copy-path = Copiar caminho copy-path = Copiar caminho
mixed = Misto

View file

@ -195,7 +195,7 @@ dark = Тёмная
light = Светлая light = Светлая
# Context menu # Context menu
add-to-sidebar = Добавить на боковую панель add-to-sidebar = Добавить на боковую панель
compress = Сжать... compress = Сжать
extract-here = Распаковать extract-here = Распаковать
new-file = Новый файл… new-file = Новый файл…
new-folder = Новая папка… new-folder = Новая папка…
@ -380,4 +380,3 @@ keywords = Папка;Менеджер;
show-recents = «Недавние документы» в бок. панели show-recents = «Недавние документы» в бок. панели
clear-recents-history = Очистить историю недавних clear-recents-history = Очистить историю недавних
copy-path = Копировать путь copy-path = Копировать путь
mixed = Смешанные

View file

@ -317,7 +317,7 @@ single-click = Відкривати одним клацанням
type-to-search = Введіть для пошуку type-to-search = Введіть для пошуку
type-to-search-recursive = Шукає у поточній теці та всіх підтеках type-to-search-recursive = Шукає у поточній теці та всіх підтеках
type-to-search-enter-path = Вводить шлях до каталогу або файлу type-to-search-enter-path = Вводить шлях до каталогу або файлу
compress = Стиснути... compress = Стиснути
delete-permanently = Остаточно видалити delete-permanently = Остаточно видалити
eject = Безпечно вилучити eject = Безпечно вилучити
extract-here = Видобути extract-here = Видобути

View file

@ -11,7 +11,7 @@ recents = 最近访问
undo = 撤销 undo = 撤销
today = 今天 today = 今天
# Desktop view options # Desktop view options
desktop-view-options = 桌面视图选项… desktop-view-options = 桌面视图选项...
show-on-desktop = 在桌面显示 show-on-desktop = 在桌面显示
desktop-folder-content = 桌面文件夹内容 desktop-folder-content = 桌面文件夹内容
mounted-drives = 已装载驱动器 mounted-drives = 已装载驱动器
@ -31,7 +31,7 @@ operations-running =
正在进行 { $running } { $running -> 正在进行 { $running } { $running ->
[one] 个操作 [one] 个操作
*[other] 个操作 *[other] 个操作
}({ $percent }%)… }({ $percent }%)...
operations-running-finished = operations-running-finished =
正在进行 { $running } { $running -> 正在进行 { $running } { $running ->
[one] 个操作 [one] 个操作
@ -50,7 +50,7 @@ create-archive = 创建压缩包
## Extract Dialog ## Extract Dialog
extract-password-required = 需要密码 extract-password-required = 需要密码
extract-to = 提取到… extract-to = 提取到...
extract-to-title = 提取到文件夹 extract-to-title = 提取到文件夹
## Empty Trash Dialog ## Empty Trash Dialog
@ -317,7 +317,7 @@ unknown-folder = 未知文件夹
## Open with ## Open with
menu-open-with = 打开方式… menu-open-with = 打开方式...
default-app = { $name }(默认) default-app = { $name }(默认)
## Show details ## Show details
@ -329,7 +329,7 @@ item-size = 文件大小:{ $size }
item-created = 创建于:{ $created } item-created = 创建于:{ $created }
item-modified = 修改于:{ $modified } item-modified = 修改于:{ $modified }
item-accessed = 访问于:{ $accessed } item-accessed = 访问于:{ $accessed }
calculating = 计算中… calculating = 计算中...
## Settings ## Settings
@ -355,8 +355,8 @@ compress = 压缩…
delete-permanently = 永久删除 delete-permanently = 永久删除
eject = 弹出 eject = 弹出
extract-here = 解压到此处 extract-here = 解压到此处
new-file = 新建文件… new-file = 新建文件...
new-folder = 新建文件夹… new-folder = 新建文件夹...
open-in-terminal = 在终端模拟器中打开 open-in-terminal = 在终端模拟器中打开
move-to-trash = 移动到回收站 move-to-trash = 移动到回收站
restore-from-trash = 从回收站中还原 restore-from-trash = 从回收站中还原
@ -369,9 +369,9 @@ remove-from-recents = 从最近访问中移除
## Desktop ## Desktop
change-wallpaper = 更改壁纸… change-wallpaper = 更改壁纸...
desktop-appearance = 桌面外观… desktop-appearance = 桌面外观...
display-settings = 显示设置… display-settings = 显示设置...
# Menu # Menu
@ -382,7 +382,7 @@ file = 文件
new-tab = 新建标签 new-tab = 新建标签
new-window = 新建窗口 new-window = 新建窗口
reload-folder = 刷新文件夹 reload-folder = 刷新文件夹
rename = 重命名… rename = 重命名...
close-tab = 关闭标签 close-tab = 关闭标签
quit = 退出 quit = 退出
@ -405,7 +405,7 @@ list-view = 列表视图
show-hidden-files = 显示隐藏文件 show-hidden-files = 显示隐藏文件
list-directories-first = 优先列出目录 list-directories-first = 优先列出目录
gallery-preview = 图库预览 gallery-preview = 图库预览
menu-settings = 设置… menu-settings = 设置...
menu-about = 关于 COSMIC 文件… menu-about = 关于 COSMIC 文件…
## Sort ## Sort
@ -435,5 +435,4 @@ comment = COSMIC 桌面的文件管理器
keywords = 文件夹;管理器; keywords = 文件夹;管理器;
clear-recents-history = 清除最近访问历史 clear-recents-history = 清除最近访问历史
copy-path = 复制文件路径 copy-path = 复制文件路径
show-recents = 侧边栏中的最近访问 show-recents = 侧边栏显示最近访问
mixed = 混合

View file

@ -1,11 +1,11 @@
cosmic-files = COSMIC 檔案 cosmic-files = COSMIC 檔案總管
empty-folder = 空資料夾 empty-folder = 空資料夾
empty-folder-hidden = 空資料夾(包含隱藏項目) empty-folder-hidden = 空資料夾(包含隱藏項目)
no-results = 找不到結果 no-results = 找不到結果
filesystem = 檔案系統 filesystem = 檔案系統
home = 家目錄 home = 主目錄
networks = 網路 networks = 網路
notification-in-progress = 檔案操作正在進行中 notification-in-progress = 檔案操作正在進行中。
trash = 垃圾桶 trash = 垃圾桶
recents = 最近使用 recents = 最近使用
undo = 復原 undo = 復原
@ -25,7 +25,7 @@ create-archive = 建立壓縮檔案
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = 清空垃圾桶 empty-trash = 清空垃圾桶
empty-trash-warning = 垃圾桶中的項目將被永久刪除 empty-trash-warning = 你確定要永久刪除垃圾桶中的所有項目嗎?
## New File/Folder Dialog ## New File/Folder Dialog
@ -33,16 +33,16 @@ create-new-file = 建立新檔案
create-new-folder = 建立新資料夾 create-new-folder = 建立新資料夾
file-name = 檔案名稱 file-name = 檔案名稱
folder-name = 資料夾名稱 folder-name = 資料夾名稱
file-already-exists = 相同名稱的檔案已經存在 file-already-exists = 已存在同名檔案。
folder-already-exists = 相同名稱的資料夾已經存在 folder-already-exists = 已存在同名資料夾。
name-hidden = 以「.」開頭的名稱將會被隱藏 name-hidden = 以「.」開頭的名稱將會被隱藏。
name-invalid = 名稱不能是「{ $filename }」 name-invalid = 名稱不能是 「{ $filename }」。
name-no-slashes = 名稱不能包含斜線 name-no-slashes = 名稱不能包含斜線。
## Open/Save Dialog ## Open/Save Dialog
cancel = 取消 cancel = 取消
create = 創作 create = 建立
open = 開啟 open = 開啟
open-file = 開啟檔案 open-file = 開啟檔案
open-folder = 開啟資料夾 open-folder = 開啟資料夾
@ -62,12 +62,12 @@ rename-folder = 重新命名資料夾
## Replace Dialog ## Replace Dialog
replace = 取代 replace = 取代
replace-title = 「{ $filename }」已存在於此位置 replace-title = 檔案「{ $filename }」已存在於此位置。
replace-warning = 你要取代它嗎?取代將覆蓋其內容。 replace-warning = 你要取代它嗎?取代將覆蓋其內容。
replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。 replace-warning-operation = 你要取代它嗎?取代將覆蓋其內容。
original-file = 原始檔案 original-file = 原始檔案
replace-with = 取代為 replace-with = 取代為
apply-to-all = 套用至全部 apply-to-all = 套用至所有項目
keep-both = 保留兩者 keep-both = 保留兩者
skip = 跳過 skip = 跳過
@ -95,13 +95,13 @@ network-drive-description =
伺服器地址包括協定前綴和地址。 伺服器地址包括協定前綴和地址。
範例:ssh://192.168.0.1, ftp://[2001:db8::1] 範例:ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-schemes = network-drive-schemes =
可用協定,前綴 可用協定,前綴
AppleTalk,afp:// AppleTalk,afp://
檔案傳輸協定,ftp:// 或 ftps:// 檔案傳輸協定,ftp:// 或 ftps://
網路檔案系統,nfs:// 網路檔案系統,nfs://
伺服器訊息區塊,smb:// 伺服器訊息區塊,smb://
SSH 檔案傳輸協定,sftp:// 或 ssh:// SSH 檔案傳輸協定,sftp:// 或 ssh://
WebDav,dav:// 或 davs:// WebDav,dav:// 或 davs://
network-drive-error = 無法存取網路磁碟機 network-drive-error = 無法存取網路磁碟機
password = 密碼 password = 密碼
remember-password = 記住密碼 remember-password = 記住密碼
@ -112,7 +112,7 @@ username = 使用者名稱
edit-history = 編輯歷史 edit-history = 編輯歷史
history = 歷史紀錄 history = 歷史紀錄
no-history = 無歷史記錄項目。 no-history = 沒有歷史項目。
pending = 待處理 pending = 待處理
failed = 失敗 failed = 失敗
complete = 完成 complete = 完成
@ -133,51 +133,51 @@ copying =
正在複製 { $items } { $items -> 正在複製 { $items } { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從「{ $from }」到「{ $to }」({ $progress })... } 從「{ $from }」到「{ $to }」({ $progress })...
copied = copied =
已複製 { $items } { $items -> 已複製 { $items } { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從「{ $from }」到「{ $to }」 }從「{ $from }」到「{ $to }」
emptying-trash = 正在清空 { trash }({ $progress })… emptying-trash = 正在清空{ trash }({ $progress })…
emptied-trash = 已經清空 { trash } emptied-trash = 已清空{ trash }
extracting = extracting =
正在解壓縮 { $items } 項目 { $items -> 正在解壓縮 { $items } 項目 { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從「{ $from }」至「{ $to }」({ $progress })... } 從「{ $from }」到「{ $to }」({ $progress })...
extracted = extracted =
已解壓縮 { $items } 項目 { $items -> 已解壓縮 { $items } 項目 { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從「{ $from }」到「{ $to }」 } 從 { $from } 到 { $to }
moving = moving =
正在移動 { $items } { $items -> 正在移動 { $items } { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從「{ $from }」到「{ $to }」({ $progress })... } 從「{ $from }」到「{ $to }」({ $progress })...
moved = moved =
已經移動 { $items } { $items -> 已移動 { $items } { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
} 從「{ $from }」至「{ $to }」 } 從「{ $from }」到「{ $to }」
renaming = 正在重新命名「{ $from }」至「{ $to }」 renaming = 正在重新命名「{ $from }」為「{ $to }」
renamed = 已經重新命名「{ $from }」至「{ $to }」 renamed = 已重新命名 { $from } 為 { $to }
restoring = restoring =
正在還原 { $items } 項目 { $items -> 正在還原 { $items } 項目 { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}自 { trash } ({ $progress })... } 從{ trash }({ $progress })...
restored = restored =
已經還原 { $items } 項目 { $items -> 已還原 { $items } 項目 { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從 { trash } } 從{ trash }
unknown-folder = 不明資料夾 unknown-folder = 未知資料夾
## Open with ## Open with
menu-open-with = 開啟檔案... menu-open-with = 開啟方式...
default-app = { $name } (預設) default-app = { $name } (預設)
## Show details ## Show details
@ -192,15 +192,15 @@ settings = 設定
appearance = 外觀 appearance = 外觀
theme = 主題 theme = 主題
match-desktop = 符合桌面 match-desktop = 與桌面一致
dark = 深色 dark = 暗色模式
light = 淺色 light = 亮色模式
# Context menu # Context menu
add-to-sidebar = 添加至側邊欄 add-to-sidebar = 加入側邊欄
compress = 壓縮… compress = 壓縮
extract-here = 解壓縮 extract-here = 解壓縮至此
new-file = 新建檔案... new-file = 新檔案...
new-folder = 新建資料夾... new-folder = 新資料夾...
open-in-terminal = 在終端機中開啟 open-in-terminal = 在終端機中開啟
move-to-trash = 移動至垃圾桶 move-to-trash = 移動至垃圾桶
restore-from-trash = 從垃圾桶還原 restore-from-trash = 從垃圾桶還原
@ -215,8 +215,8 @@ sort-by-size = 依大小排序
## File ## File
file = 檔案 file = 檔案
new-tab = 新建分頁 new-tab = 新分頁
new-window = 新建視窗 new-window = 新視窗
rename = 重新命名... rename = 重新命名...
close-tab = 關閉分頁 close-tab = 關閉分頁
quit = 退出 quit = 退出
@ -238,36 +238,36 @@ view = 檢視
grid-view = 網格檢視 grid-view = 網格檢視
list-view = 列表檢視 list-view = 列表檢視
show-hidden-files = 顯示隱藏檔案 show-hidden-files = 顯示隱藏檔案
list-directories-first = 目錄優先列出 list-directories-first = 優先列出目錄
menu-settings = 設定... menu-settings = 設定...
menu-about = 關於 COSMIC 檔案... menu-about = 關於 COSMIC 檔案總管...
## Sort ## Sort
sort = 排序 sort = 排序
sort-a-z = A-Z sort-a-z = A-Z
sort-z-a = Z-A sort-z-a = Z-A
sort-newest-first = 最新優先 sort-newest-first = 最新的在前
sort-oldest-first = 最舊優先 sort-oldest-first = 最舊的在前
sort-smallest-to-largest = 從小到大 sort-smallest-to-largest = 由小至大
sort-largest-to-smallest = 從大到小 sort-largest-to-smallest = 由大至小
deleted = deleted =
已經刪除 { $items } { $items -> 已刪除 { $items } { $items ->
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
}從 { trash } }從{ trash }
permanently-deleting = permanently-deleting =
正在永久刪除 { $items } { $items -> 正在永久刪除 { $items } { $items ->
[one] 项目 [one] 项目
*[other] 项目 *[other] 项目
} }
permanently-deleted = permanently-deleted =
已經永久刪除 { $items } { $items -> 已永久刪除 { $items } { $items ->
[one] 项目 [one] 项目
*[other] 项目 *[other] 项目
} }
removing-from-recents = removing-from-recents =
正在從 { recents } 中移除 { $items } { $items -> 正在從{ recents }中移除 { $items } { $items ->
[one] 项目 [one] 项目
*[other] 项目 *[other] 项目
} }
@ -275,112 +275,9 @@ deleting =
正在刪除 { $items } { $items -> 正在刪除 { $items } { $items ->
[one] 项目 [one] 项目
*[other] 项目 *[other] 项目
}從 { trash }({ $progress })… }從{ trash }({ $progress })…
removed-from-recents = removed-from-recents =
已經從 { recents } 中移除 { $items } { $items -> 已從{ recents }中移除 { $items } { $items ->
[one] 项目 [one] 项目
*[other] 项目 *[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 = 混合

View file

@ -18,7 +18,9 @@ use cosmic::{
Application, ApplicationExt, Element, Application, ApplicationExt, Element,
app::{self, Core, Task, context_drawer}, app::{self, Core, Task, context_drawer},
cosmic_config::{self, ConfigSet}, cosmic_config::{self, ConfigSet},
cosmic_theme, executor, cosmic_theme,
desktop::fde::DesktopEntry,
executor,
iced::core::widget::operation::focusable::unfocus, iced::core::widget::operation::focusable::unfocus,
iced::runtime::{clipboard, task}, iced::runtime::{clipboard, task},
iced::widget::{button::focus, scrollable::AbsoluteOffset}, iced::widget::{button::focus, scrollable::AbsoluteOffset},
@ -97,7 +99,6 @@ use crate::{
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK,
SearchLocation, Tab, SearchLocation, Tab,
}, },
trash::{Trash, TrashExt},
zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
}; };
@ -833,12 +834,9 @@ impl App {
// First launch apps that can be launched directly // First launch apps that can be launched directly
if mime == "application/x-desktop" { if mime == "application/x-desktop" {
#[cfg(feature = "desktop")]
{
// Try opening desktop application // Try opening desktop application
Self::launch_desktop_entries(&paths); Self::launch_desktop_entries(&paths);
continue; continue;
}
} else if mime == "application/x-executable" || mime == "application/vnd.appimage" { } else if mime == "application/x-executable" || mime == "application/vnd.appimage" {
// Try opening executable // Try opening executable
for path in paths { for path in paths {
@ -899,10 +897,7 @@ impl App {
Task::batch(tasks) Task::batch(tasks)
} }
#[cfg(feature = "desktop")]
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) { fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
use cosmic::desktop::fde::DesktopEntry;
for path in paths.iter().map(AsRef::as_ref) { for path in paths.iter().map(AsRef::as_ref) {
match DesktopEntry::from_path::<&str>(path, None) { match DesktopEntry::from_path::<&str>(path, None) {
Ok(entry) => match entry.exec() { Ok(entry) => match entry.exec() {
@ -1799,7 +1794,7 @@ impl App {
nav_model = nav_model.insert(|b| { nav_model = nav_model.insert(|b| {
b.text(fl!("trash")) b.text(fl!("trash"))
.icon(icon::icon(Trash::icon_symbolic(16))) .icon(icon::icon(tab::trash_helpers::trash_icon_symbolic(16)))
.data(Location::Trash) .data(Location::Trash)
.divider_above() .divider_above()
}); });
@ -2660,7 +2655,9 @@ impl Application for App {
)); ));
} }
if matches!(location_opt, Some(Location::Trash)) && !Trash::is_empty() { if matches!(location_opt, Some(Location::Trash))
&& !trash::os_limited::is_empty().unwrap_or(true)
{
items.push(cosmic::widget::menu::Item::Button( items.push(cosmic::widget::menu::Item::Button(
fl!("empty-trash"), fl!("empty-trash"),
None, None,
@ -4240,8 +4237,10 @@ impl Application for App {
.is_some_and(|loc| matches!(loc, Location::Trash)) .is_some_and(|loc| matches!(loc, Location::Trash))
}); });
if let Some(entity) = maybe_entity { if let Some(entity) = maybe_entity {
self.nav_model self.nav_model.icon_set(
.icon_set(entity, icon::icon(Trash::icon_symbolic(16))); entity,
icon::icon(tab::trash_helpers::trash_icon_symbolic(16)),
);
} }
return Task::batch([self.rescan_trash(), self.update_desktop()]); return Task::batch([self.rescan_trash(), self.update_desktop()]);
@ -4263,14 +4262,12 @@ impl Application for App {
.collect(); .collect();
if !selected.is_empty() { if !selected.is_empty() {
//TODO: batch rename //TODO: batch rename
let mut last_name = String::new(); let tasks = selected
let tasks: Vec<_> = selected
.into_iter() .into_iter()
.filter_map(|path| { .filter_map(|path| {
let parent = path.parent()?.to_path_buf(); let parent = path.parent()?.to_path_buf();
let name = path.file_name()?.to_str()?.to_string(); let name = path.file_name()?.to_str()?.to_string();
let dir = path.is_dir(); let dir = path.is_dir();
last_name = name.clone();
Some(self.dialog_pages.push_back(DialogPage::RenameItem { Some(self.dialog_pages.push_back(DialogPage::RenameItem {
from: path, from: path,
parent, parent,
@ -4278,15 +4275,9 @@ impl Application for App {
dir, dir,
})) }))
}) })
.collect(); .chain(std::iter::once(widget::text_input::focus(
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(), self.dialog_text_input.clone(),
&last_name, )));
'.',
),
]);
return Task::batch(tasks); return Task::batch(tasks);
} }
} }
@ -6160,7 +6151,6 @@ impl Application for App {
.into(), .into(),
widget::text_input("", name.as_str()) widget::text_input("", name.as_str())
.id(self.dialog_text_input.clone()) .id(self.dialog_text_input.clone())
.double_click_select_delimiter('.')
.on_input(move |name| { .on_input(move |name| {
Message::DialogUpdate(DialogPage::RenameItem { Message::DialogUpdate(DialogPage::RenameItem {
from: from.clone(), from: from.clone(),
@ -6804,7 +6794,14 @@ impl Application for App {
}, },
); );
match (watcher_res, Trash::folders()) { // 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()) {
(Ok(mut watcher), Ok(trash_bins)) => { (Ok(mut watcher), Ok(trash_bins)) => {
// Watch the "bins" themselves as well as the files folder where // Watch the "bins" themselves as well as the files folder where
// trashed items are placed. This allows us to avoid recursively // trashed items are placed. This allows us to avoid recursively
@ -6961,7 +6958,8 @@ impl Application for App {
|_| { |_| {
stream::channel( stream::channel(
1, 1,
move |mut msg_tx: futures::channel::mpsc::Sender<_>| async move { move |msg_tx: futures::channel::mpsc::Sender<_>| async move {
let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx));
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
match notify_rust::Notification::new() match notify_rust::Notification::new()
.summary(&fl!("notification-in-progress")) .summary(&fl!("notification-in-progress"))
@ -6971,6 +6969,8 @@ impl Application for App {
Ok(notification) => { Ok(notification) => {
let _ = futures::executor::block_on(async { let _ = futures::executor::block_on(async {
msg_tx msg_tx
.lock()
.await
.send(Message::Notification(Arc::new( .send(Message::Notification(Arc::new(
Mutex::new(notification), Mutex::new(notification),
))) )))

View file

@ -189,8 +189,6 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
if file.is_symlink() && (cfg!(unix) || cfg!(windows)) { if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
let mut target = Vec::with_capacity(file.size() as usize); let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?; file.read_to_end(&mut target)?;
// File no longer needed, drop to allow reading target on windows
drop(file);
#[cfg(unix)] #[cfg(unix)]
{ {
@ -201,15 +199,11 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
#[cfg(windows)] #[cfg(windows)]
{ {
let Ok(target) = String::from_utf8(target) else { let Ok(target) = String::from_utf8(target) else {
return Err(ZipError::InvalidArchive( return Err(ZipError::InvalidArchive("Invalid UTF-8 as symlink target"));
"Invalid UTF-8 as symlink target".into(),
));
}; };
let target_is_dir_from_archive = match password { let target = target.into_boxed_str();
None => archive.by_name(&target), let target_is_dir_from_archive =
Some(pwd) => archive.by_name_decrypt(&target, pwd.as_bytes()), archive.shared.files.contains_key(&target) && is_dir(&target);
}
.map_or(false, |x| x.is_dir());
let target_path = directory.as_ref().join(OsString::from(target.to_string())); let target_path = directory.as_ref().join(OsString::from(target.to_string()));
let target_is_dir = if target_is_dir_from_archive { let target_is_dir = if target_is_dir_from_archive {
true true

View file

@ -1,77 +0,0 @@
// 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()
}
}

View file

@ -441,8 +441,13 @@ impl<M: Send + 'static> Dialog<M> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum DialogPage { enum DialogPage {
NewFolder { parent: PathBuf, name: String }, NewFolder {
Replace { filename: String }, parent: PathBuf,
name: String,
},
Replace {
filename: String,
},
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -595,7 +600,6 @@ impl App {
col = col.push( col = col.push(
widget::text_input("", filename) widget::text_input("", filename)
.id(self.filename_id.clone()) .id(self.filename_id.clone())
.double_click_select_delimiter('.')
.on_input(Message::Filename) .on_input(Message::Filename)
.on_submit(|_| Message::Save(false)), .on_submit(|_| Message::Save(false)),
); );
@ -1958,16 +1962,6 @@ impl Application for App {
if self.search_get().is_some() { if self.search_get().is_some() {
return widget::text_input::focus(self.search_id.clone()); 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()); return widget::text_input::focus(self.filename_id.clone());
} }
} }

View file

@ -3,20 +3,14 @@
use cosmic::{app::Settings, iced::Limits}; use cosmic::{app::Settings, iced::Limits};
use std::{env, fs, path::PathBuf, process}; 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; pub mod app;
mod archive; mod archive;
pub mod channel;
pub mod clipboard; pub mod clipboard;
pub mod config;
mod context_action; mod context_action;
use config::Config;
pub mod config;
pub mod dialog; pub mod dialog;
mod key_bind; mod key_bind;
pub(crate) mod large_image; pub(crate) mod large_image;
@ -29,11 +23,13 @@ mod mounter;
mod mouse_area; mod mouse_area;
pub mod operation; pub mod operation;
mod spawn_detached; mod spawn_detached;
use tab::Location;
mod zoom;
use crate::config::State;
pub mod tab; pub mod tab;
mod thumbnail_cacher; mod thumbnail_cacher;
mod thumbnailer; mod thumbnailer;
pub(crate) mod trash;
mod zoom;
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>; pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
@ -77,22 +73,7 @@ pub fn is_wayland() -> bool {
/// Runs application in desktop mode /// Runs application in desktop mode
#[rustfmt::skip] #[rustfmt::skip]
pub fn desktop() -> Result<(), Box<dyn std::error::Error>> { pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
let log_format = tracing_subscriber::fmt::format() env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
.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(); localize::localize();
@ -127,21 +108,7 @@ pub fn desktop() -> Result<(), Box<dyn std::error::Error>> {
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
pub fn main() -> Result<(), Box<dyn std::error::Error>> { pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let log_format = tracing_subscriber::fmt::format() env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
.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(); localize::localize();
@ -191,7 +158,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
if daemonize { if daemonize {
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] #[cfg(all(unix, not(target_os = "redox")))]
match fork::daemon(true, true) { match fork::daemon(true, true) {
Ok(fork::Fork::Child) => (), Ok(fork::Fork::Child) => (),
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0), Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),

View file

@ -23,7 +23,6 @@ use crate::{
config::{Config, ContextActionPreset}, config::{Config, ContextActionPreset},
fl, fl,
tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab}, tab::{self, HeadingOptions, Location, LocationMenuAction, SearchLocation, Tab},
trash::{Trash, TrashExt},
}; };
static MENU_ID: LazyLock<cosmic::widget::Id> = static MENU_ID: LazyLock<cosmic::widget::Id> =
@ -193,7 +192,7 @@ pub fn context_menu<'a>(
) => { ) => {
if selected_trash_only { if selected_trash_only {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open).into());
if !Trash::is_empty() { if !trash::os_limited::is_empty().unwrap_or(true) {
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into());
} }
} else if let Some(entry) = selected_desktop_entry { } else if let Some(entry) = selected_desktop_entry {

View file

@ -19,7 +19,6 @@ struct MimeIconKey {
struct MimeIconCache { struct MimeIconCache {
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>, cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo, shared_mime_info: xdg_mime::SharedMimeInfo,
} }
@ -27,17 +26,10 @@ impl MimeIconCache {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
cache: FxHashMap::default(), cache: FxHashMap::default(),
#[cfg(unix)]
shared_mime_info: xdg_mime::SharedMimeInfo::new(), 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> { pub fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
self.cache self.cache
.entry(key) .entry(key)
@ -61,16 +53,6 @@ impl MimeIconCache {
static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> = static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
LazyLock::new(|| Mutex::new(MimeIconCache::new())); 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( pub fn mime_for_path(
path: impl AsRef<Path>, path: impl AsRef<Path>,
metadata_opt: Option<&fs::Metadata>, metadata_opt: Option<&fs::Metadata>,
@ -118,13 +100,8 @@ 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>> { pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
mime_icon_cache.shared_mime_info.get_parents_aliased(mime) mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
} }

View file

@ -5,7 +5,7 @@ use cosmic::{
}; };
use gio::{glib, prelude::*}; use gio::{glib, prelude::*};
use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc}; use std::{any::TypeId, cell::Cell, future::pending, hash::Hash, path::PathBuf, sync::Arc};
use tokio::sync::mpsc; use tokio::sync::{Mutex, mpsc};
use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage};
use crate::{ use crate::{
@ -199,7 +199,6 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
metadata, metadata,
hidden, hidden,
location_opt: Some(location), location_opt: Some(location),
image_dimensions: None,
mime, mime,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
@ -230,10 +229,7 @@ fn dir_info(uri: &str) -> Result<(String, String, Option<PathBuf>), glib::Error>
Ok((resolved_uri, info.display_name().into(), file.path())) Ok((resolved_uri, info.display_name().into(), file.path()))
} }
fn mount_op( fn mount_op(uri: String, event_tx: mpsc::UnboundedSender<Event>) -> gio::MountOperation {
uri: String,
event_tx: std::sync::Weak<crate::channel::Sender<Event>>,
) -> gio::MountOperation {
let mount_op = gio::MountOperation::new(); let mount_op = gio::MountOperation::new();
mount_op.connect_ask_password( mount_op.connect_ask_password(
move |mount_op, message, default_user, default_domain, flags| { move |mount_op, message, default_user, default_domain, flags| {
@ -256,9 +252,9 @@ fn mount_op(
.then_some(false), .then_some(false),
}; };
let (auth_tx, mut auth_rx) = mpsc::channel(1); let (auth_tx, mut auth_rx) = mpsc::channel(1);
if let Some(event_tx) = event_tx.upgrade() { event_tx
event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)); .send(Event::NetworkAuth(uri.clone(), auth, auth_tx))
} .unwrap();
//TODO: async recv? //TODO: async recv?
if let Some(auth) = auth_rx.blocking_recv() { if let Some(auth) = auth_rx.blocking_recv() {
if auth.anonymous_opt == Some(true) { if auth.anonymous_opt == Some(true) {
@ -361,45 +357,37 @@ impl Item {
pub struct Gvfs { pub struct Gvfs {
command_tx: mpsc::UnboundedSender<Cmd>, command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<crate::channel::Receiver<Event>>, event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
} }
impl Gvfs { impl Gvfs {
pub fn new() -> Self { pub fn new() -> Self {
//TODO: switch to using gvfs-zbus which will better integrate with async rust //TODO: switch to using gvfs-zbus which will better integrate with async rust
let (command_tx, mut command_rx) = mpsc::unbounded_channel(); let (command_tx, mut command_rx) = mpsc::unbounded_channel();
let (event_tx, event_rx) = crate::channel::channel(); let (event_tx, event_rx) = mpsc::unbounded_channel();
let event_tx = Arc::new(event_tx);
std::thread::spawn(move || { std::thread::spawn(move || {
let main_loop = glib::MainLoop::new(None, false); let main_loop = glib::MainLoop::new(None, false);
main_loop.context().spawn_local(async move { main_loop.context().spawn_local(async move {
let event_tx = Arc::downgrade(&event_tx);
let monitor = gio::VolumeMonitor::get(); let monitor = gio::VolumeMonitor::get();
{ {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_mount_changed(move |_monitor, mount| { monitor.connect_mount_changed(move |_monitor, mount| {
log::info!("mount changed {}", MountExt::name(mount)); log::info!("mount changed {}", MountExt::name(mount));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
{ {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_mount_added(move |_monitor, mount| { monitor.connect_mount_added(move |_monitor, mount| {
log::info!("mount added {}", MountExt::name(mount)); log::info!("mount added {}", MountExt::name(mount));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
{ {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_mount_removed(move |_monitor, mount| { monitor.connect_mount_removed(move |_monitor, mount| {
log::info!("mount removed {}", MountExt::name(mount)); log::info!("mount removed {}", MountExt::name(mount));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
@ -407,27 +395,21 @@ impl Gvfs {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_volume_changed(move |_monitor, volume| { monitor.connect_volume_changed(move |_monitor, volume| {
log::info!("volume changed {}", VolumeExt::name(volume)); log::info!("volume changed {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
{ {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_volume_added(move |_monitor, volume| { monitor.connect_volume_added(move |_monitor, volume| {
log::info!("volume added {}", VolumeExt::name(volume)); log::info!("volume added {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
{ {
let event_tx = event_tx.clone(); let event_tx = event_tx.clone();
monitor.connect_volume_removed(move |_monitor, volume| { monitor.connect_volume_removed(move |_monitor, volume| {
log::info!("volume removed {}", VolumeExt::name(volume)); log::info!("volume removed {}", VolumeExt::name(volume));
if let Some(event_tx) = event_tx.upgrade() { event_tx.send(Event::Changed).unwrap();
event_tx.send(Event::Changed);
}
}); });
} }
@ -437,11 +419,7 @@ impl Gvfs {
items_tx.send(items(&monitor, sizes)).await.unwrap(); items_tx.send(items(&monitor, sizes)).await.unwrap();
} }
Cmd::Rescan => { Cmd::Rescan => {
let Some(event_tx) = event_tx.upgrade() else { event_tx.send(Event::Items(items(&monitor, IconSizes::default()))).unwrap();
return;
};
event_tx.send(Event::Items(items(&monitor, IconSizes::default())));
} }
Cmd::Mount(mounter_item, complete_tx) => { Cmd::Mount(mounter_item, complete_tx) => {
let MounterItem::Gvfs(ref item) = mounter_item else { let MounterItem::Gvfs(ref item) = mounter_item else {
@ -493,9 +471,6 @@ impl Gvfs {
.ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE)) .ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.unwrap_or(true); .unwrap_or(true);
} }
let Some(event_tx) = event_tx.upgrade() else {
return;
};
event_tx.send(Event::MountResult(updated_item, match res { event_tx.send(Event::MountResult(updated_item, match res {
Ok(()) => { Ok(()) => {
_ = complete_tx.send(Ok(())); _ = complete_tx.send(Ok(()));
@ -507,7 +482,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false), Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}")) _ => Err(format!("{err}"))
}} }}
})); })).unwrap();
}, },
); );
break; break;
@ -523,9 +498,6 @@ impl Gvfs {
gio::Cancellable::NONE, gio::Cancellable::NONE,
move |res| { move |res| {
log::info!("network drive {uri}: result {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 { event_tx.send(Event::NetworkResult(uri, match res {
Ok(()) => { Ok(()) => {
_ = result_tx.send(Ok(())); _ = result_tx.send(Ok(()));
@ -536,7 +508,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false), Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}")) _ => Err(format!("{err}"))
}} }}
})); })).unwrap();
} }
); );
} }
@ -560,9 +532,6 @@ impl Gvfs {
// FIXME sometimes a uri can be mounted and then not recognized as mounted... // FIXME sometimes a uri can be mounted and then not recognized as mounted...
// seems to be related to uri with a path // seems to be related to uri with a path
items_tx.blocking_send(network_scan(&uri, sizes)).unwrap(); 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 { event_tx.send(Event::NetworkResult(resolved_uri, match res {
Ok(()) => { Ok(()) => {
Ok(true) Ok(true)
@ -571,7 +540,7 @@ impl Gvfs {
Some(gio::IOErrorEnum::FailedHandled) => Ok(false), Some(gio::IOErrorEnum::FailedHandled) => Ok(false),
_ => Err(format!("{err}")) _ => Err(format!("{err}"))
} }
})); })).unwrap();
} }
); );
} else { } else {
@ -627,7 +596,7 @@ impl Gvfs {
}); });
Self { Self {
command_tx, command_tx,
event_rx: Arc::new(event_rx), event_rx: Arc::new(Mutex::new(event_rx)),
} }
} }
} }
@ -701,7 +670,7 @@ impl Mounter for Gvfs {
let event_rx = self.event_rx.clone(); let event_rx = self.event_rx.clone();
struct Wrapper { struct Wrapper {
command_tx: mpsc::UnboundedSender<Cmd>, command_tx: mpsc::UnboundedSender<Cmd>,
event_rx: Arc<crate::channel::Receiver<Event>>, event_rx: Arc<Mutex<mpsc::UnboundedReceiver<Event>>>,
} }
impl Hash for Wrapper { impl Hash for Wrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
@ -725,7 +694,7 @@ impl Mounter for Gvfs {
MounterMessage, MounterMessage,
>| async move { >| async move {
command_tx.send(Cmd::Rescan).unwrap(); command_tx.send(Cmd::Rescan).unwrap();
while let Some(event) = event_rx.recv().await { while let Some(event) = event_rx.lock().await.recv().await {
match event { match event {
Event::Changed => command_tx.send(Cmd::Rescan).unwrap(), Event::Changed => command_tx.send(Cmd::Rescan).unwrap(),
Event::Items(items) => { Event::Items(items) => {

View file

@ -1,11 +1,7 @@
use atomic_float::AtomicF32; use std::sync::{Arc, Mutex};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::sync::Arc;
use std::sync::atomic::{self, AtomicU16};
use tokio::sync::Notify; use tokio::sync::Notify;
#[derive(Clone, Copy, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u16)]
pub enum ControllerState { pub enum ControllerState {
Cancelled, Cancelled,
Failed, Failed,
@ -15,8 +11,8 @@ pub enum ControllerState {
#[derive(Debug)] #[derive(Debug)]
struct ControllerInner { struct ControllerInner {
state: AtomicU16, state: Mutex<ControllerState>,
progress: AtomicF32, progress: Mutex<f32>,
notify: Notify, notify: Notify,
} }
@ -31,8 +27,8 @@ impl Default for Controller {
Self { Self {
primary: true, primary: true,
inner: Arc::new(ControllerInner { inner: Arc::new(ControllerInner {
state: AtomicU16::new(ControllerState::Running.into()), state: Mutex::new(ControllerState::Running),
progress: AtomicF32::new(0.0), progress: Mutex::new(0.0),
notify: Notify::new(), notify: Notify::new(),
}), }),
} }
@ -54,24 +50,19 @@ impl Controller {
} }
pub fn progress(&self) -> f32 { pub fn progress(&self) -> f32 {
self.inner.progress.load(atomic::Ordering::Relaxed) *self.inner.progress.lock().unwrap()
} }
pub fn set_progress(&self, progress: f32) { pub fn set_progress(&self, progress: f32) {
self.inner *self.inner.progress.lock().unwrap() = progress;
.progress
.swap(progress, atomic::Ordering::Relaxed);
} }
pub fn state(&self) -> ControllerState { pub fn state(&self) -> ControllerState {
ControllerState::try_from(self.inner.state.load(atomic::Ordering::Relaxed)) *self.inner.state.lock().unwrap()
.unwrap_or(ControllerState::Failed)
} }
pub fn set_state(&self, state: ControllerState) { pub fn set_state(&self, state: ControllerState) {
self.inner *self.inner.state.lock().unwrap() = state;
.state
.store(state.into(), atomic::Ordering::Relaxed);
self.inner.notify.notify_waiters(); self.inner.notify.notify_waiters();
} }
@ -95,35 +86,6 @@ impl Controller {
self.set_state(ControllerState::Paused); 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) { pub fn unpause(&self) {
if !self.is_cancelled() | !self.is_failed() { if !self.is_cancelled() | !self.is_failed() {
self.set_state(ControllerState::Running); self.set_state(ControllerState::Running);

View file

@ -22,9 +22,6 @@ use zip::AesMode::Aes256;
pub use self::controller::{Controller, ControllerState}; pub use self::controller::{Controller, ControllerState};
pub mod controller; pub mod controller;
pub use notifiers::*;
mod notifiers;
pub use self::reader::OpReader; pub use self::reader::OpReader;
pub mod reader; pub mod reader;
@ -114,7 +111,7 @@ async fn copy_or_move(
); );
// Handle duplicate file names by renaming paths // Handle duplicate file names by renaming paths
let from_to_pairs_iter = paths let mut from_to_pairs: Vec<(PathBuf, PathBuf)> = paths
.into_iter() .into_iter()
.zip(std::iter::repeat(to.as_path())) .zip(std::iter::repeat(to.as_path()))
.filter_map(|(from, to)| { .filter_map(|(from, to)| {
@ -132,23 +129,23 @@ async fn copy_or_move(
//TODO: how to handle from missing file name? //TODO: how to handle from missing file name?
None None
} }
}); })
.collect();
// Attempt quick and simple renames // Attempt quick and simple renames
//TODO: allow rename to be used for directories in recursive context? //TODO: allow rename to be used for directories in recursive context?
if matches!(method, Method::Move { .. }) {
let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) { from_to_pairs.retain(|(from, to)| {
from_to_pairs_iter
.map(|(from, to)| async move {
//TODO: show replace dialog here? //TODO: show replace dialog here?
if to.exists() { if to.exists() {
return Some((from, to)); return true;
} }
match compio::fs::rename(&from, &to).await { //TODO: use compio::fs::rename?
match fs::rename(from, to) {
Ok(()) => { Ok(()) => {
log::info!("renamed {} to {}", from.display(), to.display()); log::info!("renamed {} to {}", from.display(), to.display());
None false
} }
Err(err) => { Err(err) => {
log::info!( log::info!(
@ -157,21 +154,11 @@ async fn copy_or_move(
to.display(), to.display(),
err err
); );
Some((from, to)) true
} }
} }
}) });
.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()); let mut context = Context::new(controller.clone());
@ -229,7 +216,7 @@ pub async fn sync_to_disk(
} }
})) }))
.buffer_unordered(32) .buffer_unordered(32)
.collect::<()>() .collect::<Vec<_>>()
.await; .await;
// Sync directories to disk // Sync directories to disk
@ -239,7 +226,7 @@ pub async fn sync_to_disk(
} }
})) }))
.buffer_unordered(16) .buffer_unordered(16)
.collect::<()>() .collect::<Vec<_>>()
.await; .await;
} }
@ -1125,9 +1112,7 @@ impl Operation {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Self::Restore { .. } => { Self::Restore { .. } => {
// TODO: add support for macos // TODO: add support for macos
return Err(OperationError::from_msg( return OperationError::from_msg("Restoring from trash is not supported on macos");
"Restoring from trash is not supported on macos",
));
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
Self::Restore { items } => { Self::Restore { items } => {

View file

@ -1,58 +0,0 @@
// 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)
}

View file

@ -1,33 +1,15 @@
// 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::BufResult;
use compio::buf::{IntoInner, IoBuf}; use compio::buf::{IntoInner, IoBuf};
use compio::driver::{ToSharedFd, op::AsyncifyFd};
use compio::io::{AsyncReadAt, AsyncWriteAt}; use compio::io::{AsyncReadAt, AsyncWriteAt};
use cosmic::iced::futures;
use futures::{FutureExt, StreamExt};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::rc::Rc;
use std::time::Instant; use std::time::Instant;
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf}; use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
use walkdir::WalkDir; use walkdir::WalkDir;
#[cfg(feature = "gvfs")] use crate::operation::{OperationError, sync_to_disk};
use gio::prelude::FileExtManual;
#[derive(thiserror::Error, Debug)] use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
pub enum GioCopyError {
#[error("controller state")]
Controller(OperationError),
#[cfg(feature = "gvfs")]
#[error("gio copy failed")]
GLib(#[from] glib::Error),
}
pub enum Method { pub enum Method {
Copy, Copy,
@ -331,28 +313,136 @@ impl Op {
}) })
} }
async fn run(&mut self, ctx: &mut Context, progress: Progress) -> Result<bool, Box<dyn Error>> { async fn run(
&mut self,
ctx: &mut Context,
mut progress: Progress,
) -> Result<bool, Box<dyn Error>> {
if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) { if self.skipped.normal.get() || (self.is_cleanup && self.skipped.cleanup.get()) {
return Ok(true); return Ok(true);
} }
match self.kind { match self.kind {
OpKind::Copy => { OpKind::Copy => {
crate::operation::actively_writing_add(self.to.clone()); // Remove `to` if overwriting and it is an existing file
let result = self.copy(ctx, progress).await; if self.to.is_file() {
match ctx.replace(self).await? {
if result.is_err() { ControlFlow::Continue(to) => {
_ = compio::fs::remove_file(&self.to).await; self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
} }
crate::operation::actively_writing_remove(&self.to); let (from_file, metadata, mut to_file) = cosmic::iced::futures::try_join!(
return result; 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);
}
}
}
} }
OpKind::Move { cross_device_copy } => { 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 // Remove `to` if overwriting and it is an existing file
if self.to.is_file() { if self.to.is_file() {
match ctx.replace(self).await? { match ctx.replace(self).await? {
@ -430,268 +520,4 @@ impl Op {
} }
Ok(true) 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);
}
}
}
}
} }

View file

@ -1,7 +1,7 @@
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::{DesktopEntry, get_languages_from_env};
use cosmic::{ use cosmic::{
Apply, Element, cosmic_theme, font, Apply, Element, cosmic_theme,
desktop::fde::{DesktopEntry, get_languages_from_env},
font,
iced::core::{mouse::ScrollDelta, widget::tree}, iced::core::{mouse::ScrollDelta, widget::tree},
iced::{ iced::{
Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector, Alignment, Border, Color, ContentFit, Length, Point, Rectangle, Size, Subscription, Vector,
@ -39,10 +39,9 @@ use icu::{
use image::{DynamicImage, ImageReader}; use image::{DynamicImage, ImageReader};
use jiff_icu::ConvertFrom; use jiff_icu::ConvertFrom;
use mime_guess::{Mime, mime}; use mime_guess::{Mime, mime};
use regex::Regex;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::Cell, cell::Cell,
@ -53,6 +52,7 @@ use std::{
fs::{self, File, Metadata}, fs::{self, File, Metadata},
hash::Hash, hash::Hash,
io::{BufRead, BufReader}, io::{BufRead, BufReader},
os::unix::fs::MetadataExt,
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
sync::{Arc, LazyLock, RwLock, atomic}, sync::{Arc, LazyLock, RwLock, atomic},
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
@ -84,8 +84,8 @@ use crate::{
operation::{Controller, OperationError}, operation::{Controller, OperationError},
thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize}, thumbnail_cacher::{CachedThumbnail, ThumbnailCacher, ThumbnailSize},
thumbnailer::thumbnailer, 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 DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500);
pub const HOVER_DURATION: Duration = Duration::from_millis(1600); pub const HOVER_DURATION: Duration = Duration::from_millis(1600);
@ -557,12 +557,6 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
FsKind::Local 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> { fn get_desktop_file_display_name(path: &Path) -> Option<String> {
let locales = get_languages_from_env(); let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) { let entry = match DesktopEntry::from_path(path, Some(&locales)) {
@ -576,12 +570,6 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
entry.name(&locales).map(|s| s.into_owned()) 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> { fn get_desktop_file_icon(path: &Path) -> Option<String> {
let entry = match DesktopEntry::from_path::<&str>(path, None) { let entry = match DesktopEntry::from_path::<&str>(path, None) {
Ok(ok) => ok, Ok(ok) => ok,
@ -605,7 +593,6 @@ 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>) { pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
let locales = get_languages_from_env(); let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) { let entry = match DesktopEntry::from_path(path, Some(&locales)) {
@ -721,9 +708,6 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
children_opt, children_opt,
}, },
hidden, hidden,
image_dimensions: (!remote && mime.type_() == mime::IMAGE)
.then(|| image::image_dimensions(&path).ok())
.flatten(),
location_opt: Some(Location::Path(path)), location_opt: Some(Location::Path(path)),
mime, mime,
icon_handle_grid, icon_handle_grid,
@ -859,7 +843,6 @@ pub fn item_from_entry(
}, },
hidden, hidden,
location_opt: Some(Location::Path(path)), location_opt: Some(Location::Path(path)),
image_dimensions: None,
mime, mime,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
@ -913,9 +896,6 @@ pub fn item_from_trash_entry(
metadata: ItemMetadata::Trash { metadata, entry }, metadata: ItemMetadata::Trash { metadata, entry },
hidden: false, hidden: false,
location_opt: None, location_opt: None,
image_dimensions: (mime.type_() == mime::IMAGE)
.then(|| image::image_dimensions(&original_path).ok())
.flatten(),
mime, mime,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
@ -1181,7 +1161,133 @@ pub fn scan_search<F: Fn(SearchItem) -> bool + Sync>(
} }
} }
SearchLocation::Trash => { SearchLocation::Trash => {
Trash::scan_search(callback, &regex); trash_helpers::scan_search_trash(callback, &regex);
}
}
}
// 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;
}
}
} }
} }
} }
@ -1319,15 +1425,15 @@ pub fn scan_desktop(
let display_name = Item::display_name(&name); let display_name = Item::display_name(&name);
let metadata = ItemMetadata::SimpleDir { let metadata = ItemMetadata::SimpleDir {
entries: Trash::entries() as u64, entries: trash_helpers::trash_entries() as u64,
}; };
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
( (
"inode/directory".parse().unwrap(), "inode/directory".parse().unwrap(),
Trash::icon(sizes.grid()), trash_helpers::trash_icon(sizes.grid()),
Trash::icon(sizes.list()), trash_helpers::trash_icon(sizes.list()),
Trash::icon(sizes.list_condensed()), trash_helpers::trash_icon(sizes.list_condensed()),
) )
}; };
@ -1338,7 +1444,6 @@ pub fn scan_desktop(
metadata, metadata,
hidden: false, hidden: false,
location_opt: Some(Location::Trash), location_opt: Some(Location::Trash),
image_dimensions: None,
mime, mime,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
@ -1568,7 +1673,7 @@ impl Location {
// Search is done incrementally // Search is done incrementally
Vec::new() Vec::new()
} }
Self::Trash => Trash::scan(sizes), Self::Trash => trash_helpers::scan_trash(sizes),
Self::Recents => scan_recents(sizes), Self::Recents => scan_recents(sizes),
Self::Network(uri, _, _) => scan_network(uri, sizes), Self::Network(uri, _, _) => scan_network(uri, sizes),
}; };
@ -2214,7 +2319,6 @@ pub struct Item {
pub hidden: bool, pub hidden: bool,
pub location_opt: Option<Location>, pub location_opt: Option<Location>,
pub mime: Mime, pub mime: Mime,
pub image_dimensions: Option<(u32, u32)>,
pub icon_handle_grid: widget::icon::Handle, pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle,
pub icon_handle_list_condensed: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle,
@ -2429,7 +2533,7 @@ impl Item {
let mode = metadata.mode(); let mode = metadata.mode();
let user_name = uzers::get_user_by_uid(metadata.uid()) let user_name = get_user_by_uid(metadata.uid())
.and_then(|user| user.name().to_str().map(ToOwned::to_owned)) .and_then(|user| user.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default(); .unwrap_or_default();
let user_path = path.clone(); let user_path = path.clone();
@ -2452,7 +2556,7 @@ impl Item {
)), )),
); );
let group_name = uzers::get_group_by_gid(metadata.gid()) let group_name = get_group_by_gid(metadata.gid())
.and_then(|group| group.name().to_str().map(ToOwned::to_owned)) .and_then(|group| group.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default(); .unwrap_or_default();
let group_path = path.clone(); let group_path = path.clone();
@ -4340,7 +4444,6 @@ impl Tab {
for item in self.items_opt().map_or(Vec::new(), |items| { for item in self.items_opt().map_or(Vec::new(), |items| {
items.iter().filter(|item| item.selected).collect() items.iter().filter(|item| item.selected).collect()
}) { }) {
#[cfg(unix)]
if let (Some(path), Some(mode)) = ( if let (Some(path), Some(mode)) = (
item.path_opt(), item.path_opt(),
item.file_metadata() item.file_metadata()
@ -6358,17 +6461,15 @@ impl Tab {
} else { } else {
total_size = total_size.saturating_add(metadata.len()); total_size = total_size.saturating_add(metadata.len());
} }
#[cfg(unix)]
{
let mode = metadata.mode(); let mode = metadata.mode();
user_name.insert( user_name.insert(
uzers::get_user_by_uid(metadata.uid()) get_user_by_uid(metadata.uid())
.and_then(|user| user.name().to_str().map(ToOwned::to_owned)) .and_then(|user| user.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default(), .unwrap_or_default(),
); );
mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER)); mode_user.insert(get_mode_part(mode, MODE_SHIFT_USER));
group_name.insert( group_name.insert(
uzers::get_group_by_gid(metadata.gid()) get_group_by_gid(metadata.gid())
.and_then(|group| group.name().to_str().map(ToOwned::to_owned)) .and_then(|group| group.name().to_str().map(ToOwned::to_owned))
.unwrap_or_default(), .unwrap_or_default(),
); );
@ -6376,7 +6477,6 @@ impl Tab {
mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER)); mode_other.insert(get_mode_part(mode, MODE_SHIFT_OTHER));
} }
} }
}
let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect(); let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect();
mime_types.sort_by(|(_, v1), (_, v2)| v2.cmp(v1)); mime_types.sort_by(|(_, v1), (_, v2)| v2.cmp(v1));
@ -6613,13 +6713,13 @@ impl Tab {
// Determine effective memory budget based on image size // Determine effective memory budget based on image size
let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE { let (effective_max_mb, effective_jobs) = if mime.type_() == mime::IMAGE {
match item.image_dimensions { match image::image_dimensions(&path) {
Some((width, height)) => { Ok((width, height)) => {
let (_use_dedicated, eff_mb, eff_jobs) = let (_use_dedicated, eff_mb, eff_jobs) =
should_use_dedicated_worker(width, height, max_mb, max_jobs); should_use_dedicated_worker(width, height, max_mb, max_jobs);
(eff_mb, eff_jobs) (eff_mb, eff_jobs)
} }
None => (max_mb, max_jobs), Err(_) => (max_mb, max_jobs),
} }
} else { } else {
(max_mb, max_jobs) (max_mb, max_jobs)
@ -6662,10 +6762,6 @@ impl Tab {
stream::channel( stream::channel(
1, 1,
move |mut output: futures::channel::mpsc::Sender<_>| async move { 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 message = {
let path = path.clone(); let path = path.clone();
@ -6850,8 +6946,9 @@ impl Tab {
.await .await
.unwrap(); .unwrap();
let (watch_tx, mut watch_rx) = tokio::sync::watch::channel(true); let output = Arc::new(tokio::sync::Mutex::new(output));
{ {
let output = output.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
scan_search( scan_search(
&search_location, &search_location,
@ -6879,7 +6976,14 @@ impl Tab {
true true
} else { } else {
// Wake up update method // Wake up update method
watch_tx.send(false).is_ok() futures::executor::block_on(async {
output
.lock()
.await
.send(Message::SearchReady(false))
.await
})
.is_ok()
} }
} }
Err(_) => false, Err(_) => false,
@ -6892,16 +6996,13 @@ impl Tab {
search_location, search_location,
start.elapsed(), 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 // Send final ready
let _ = output.send(Message::SearchReady(true)).await; let _ = output.lock().await.send(Message::SearchReady(true)).await;
std::future::pending().await std::future::pending().await
}, },

View file

@ -1,12 +1,11 @@
use image::DynamicImage; use image::DynamicImage;
use md5::{Digest, Md5}; use md5::{Digest, Md5};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{ use std::{
error::Error, error::Error,
fs::{self, File}, fs::{self, File},
io::{self, BufReader, BufWriter}, io::{self, BufReader, BufWriter},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::LazyLock, sync::LazyLock,
time::UNIX_EPOCH, time::UNIX_EPOCH,
@ -94,7 +93,6 @@ impl ThumbnailCacher {
} }
pub fn update_with_temp_file(&self, temp_file: NamedTempFile) -> Result<&Path, Box<dyn Error>> { 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))?; fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?;
self.update_thumbnail_text_metadata(temp_file.path())?; self.update_thumbnail_text_metadata(temp_file.path())?;
fs::rename(temp_file.path(), &self.thumbnail_path)?; fs::rename(temp_file.path(), &self.thumbnail_path)?;
@ -129,7 +127,6 @@ impl ThumbnailCacher {
pub fn create_fail_marker(&self) -> Result<(), Box<dyn Error>> { pub fn create_fail_marker(&self) -> Result<(), Box<dyn Error>> {
if let Some(dir) = self.thumbnail_fail_marker_path.parent() { if let Some(dir) = self.thumbnail_fail_marker_path.parent() {
fs::create_dir_all(dir)?; fs::create_dir_all(dir)?;
#[cfg(unix)]
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?; fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
} }

View file

@ -1,7 +1,6 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
#[cfg(feature = "desktop")]
use cosmic::desktop::fde::GenericEntry; use cosmic::desktop::fde::GenericEntry;
use mime_guess::Mime; use mime_guess::Mime;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;

View file

@ -1,145 +0,0 @@
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 {}