Merge branch 'master' into master

This commit is contained in:
Levi Portenier 2026-02-03 12:09:32 -07:00 • committed by GitHub
commit eb1218a0db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 2278 additions and 1660 deletions

972
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
[package] [package]
name = "cosmic-files" name = "cosmic-files"
version = "1.0.0" version = "1.0.5"
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"
rust-version = "1.85" rust-version = "1.90"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
@ -14,8 +14,6 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c
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" env_logger = "0.11"
freedesktop_entry_parser = "1.3"
futures = "0.3.31"
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"
@ -24,9 +22,9 @@ image = "0.25"
libc = "0.2" libc = "0.2"
log = "0.4" log = "0.4"
mime_guess = "2" mime_guess = "2"
notify-debouncer-full = "0.6" notify-debouncer-full = "0.7"
notify-rust = { version = "4", optional = true } notify-rust = { version = "4", optional = true }
open = "5.3.2" open = "5.3.3"
paste = "1.0" paste = "1.0"
regex = "1" regex = "1"
rustc-hash = "2.1" rustc-hash = "2.1"
@ -38,15 +36,15 @@ tokio = { version = "1", features = ["process", "sync"] }
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" } trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" }
url = "2.5" url = "2.5"
walkdir = "2.5.0" walkdir = "2.5.0"
wayland-client = { version = "0.31.11", optional = true } wayland-client = { version = "0.31.12", optional = true }
xdg = { version = "3.0", optional = true } xdg = { version = "3.0", optional = true }
xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" } xdg-mime = { git = "https://github.com/ebassi/xdg-mime-rs" }
# Compression # Compression
bzip2 = { version = "0.6", optional = true } #TODO: replace with pure Rust crate bzip2 = { version = "0.6", optional = true } #TODO: replace with pure Rust crate
flate2 = "1.1" flate2 = "1.1"
tar = "0.4.44" tar = "0.4.44"
lzma-rust2 = { version = "0.15.4", optional = true } lzma-rust2 = { version = "0.15.7", optional = true }
ordermap = { version = "1.0.0", features = ["serde"] } ordermap = { version = "1.1.0", features = ["serde"] }
# Internationalization # Internationalization
i18n-embed = { version = "0.16", features = [ i18n-embed = { version = "0.16", features = [
"fluent-system", "fluent-system",
@ -54,10 +52,10 @@ i18n-embed = { version = "0.16", features = [
] } ] }
i18n-embed-fl = "0.10" i18n-embed-fl = "0.10"
rust-embed = "8" rust-embed = "8"
slotmap = "1.0.7" slotmap = "1.1.1"
recently-used-xbel = { git = "https://github.com/pop-os/recently-used-xbel.git" } recently-used-xbel = { git = "https://github.com/pop-os/recently-used-xbel.git" }
zip = "7" zip = "7"
uzers = "0.12.1" 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"] }
@ -65,22 +63,23 @@ num_cpus = "1.17.0"
# 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]
# Patched to fix mtime: https://github.com/compio-rs/compio/pull/625 version = "0.18"
# version = "0.17.0"
git = "https://github.com/jackpot51/compio.git"
default-features = false default-features = false
features = ["fs", "io", "macros", "polling", "runtime"] features = ["fs", "io", "macros", "polling", "runtime"]
[dependencies.io-uring]
version = "0.7.11"
default-features = false
optional = true
[dependencies.libcosmic] [dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git" git = "https://github.com/pop-os/libcosmic.git"
default-features = false default-features = false
#TODO: a11y feature crashes #TODO: a11y feature crashes
features = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"] features = [
"about",
"autosize",
"desktop",
"multi-window",
"tokio",
"winit",
"surface-message",
]
[[example]] [[example]]
name = "gio-list" name = "gio-list"
@ -107,11 +106,10 @@ default = [
"wayland", "wayland",
] ]
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", "dep:io-uring"] io-uring = ["compio/io-uring"]
io-uring-bindgen = ["io-uring?/bindgen"]
jemalloc = ["dep:tikv-jemallocator"] jemalloc = ["dep:tikv-jemallocator"]
notify = ["dep:notify-rust"] notify = ["dep:notify-rust"]
wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"] wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"]

View file

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

12
debian/changelog vendored
View file

@ -1,3 +1,15 @@
cosmic-files (1.0.5) noble; urgency=medium
* Epoch 1.0.5 version update
-- Jeremy Soller <jeremy@system76.com> Fri, 30 Jan 2026 17:16:28 -0700
cosmic-files (1.0.4) noble; urgency=medium
* Epoch 1.0.4 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 21 Jan 2026 10:16:11 -0700
cosmic-files (1.0.0) jammy; urgency=medium cosmic-files (1.0.0) jammy; urgency=medium
* Stable release. * Stable release.

View file

@ -389,9 +389,9 @@ type-to-search-enter-path = Zadává cestu ke složce nebo souboru
compress = Komprimovat compress = Komprimovat
eject = Vysunout eject = Vysunout
extract-here = Extrahovat extract-here = Extrahovat
change-wallpaper = Změnit pozadí... change-wallpaper = Změnit tapetu...
desktop-appearance = Vzhled plochy... desktop-appearance = Vzhled plochy...
display-settings = Nastavení displeje... display-settings = Nastavení obrazovky...
reload-folder = Znovu načíst složku reload-folder = Znovu načíst složku
sort-z-a = Z-A sort-z-a = Z-A
sort-newest-first = Nejnovější první sort-newest-first = Nejnovější první

View file

@ -0,0 +1,306 @@
empty-folder = Kosongkan map
empty-folder-hidden = Kosongkan map (memiliki item tersembunyi)
no-results = Tidak ada hasil yang ditemukan
filesystem = Sistem berkas
cosmic-files = Berkas COSMIC
home = Beranda
networks = Jaringan
notification-in-progress = Operasi berkas sedang berlangsung
trash = Sampah
recents = Terbaru
undo = Batalkan
today = Hari ini
desktop-view-options = Opsi tampilan desktop...
show-on-desktop = Tampilkan di Desktop
desktop-folder-content = Konten map desktop
mounted-drives = Drive terpasang
trash-folder-icon = Ikon map sampah
icon-size-and-spacing = Ukuran dan jarak ikon
icon-size = Ukuran ikon
name = Nama
grid-spacing = Jarak antar kisi
modified = Dimodifikasi
trashed-on = Dibuang
size = Ukuran
details = Rincian
dismiss = Abaikan pesan
operations-running =
{ $running } { $running ->
[one] operasi
*[other] operasi
} berjalan ({ $percent }%)...
operations-running-finished =
{ $running } { $running ->
[one] operasi
*[other] operasi
} berjalan ({ $percent }%), { $finished } selesai...
pause = Jeda
resume = Lanjutkan
create-archive = Buat arsip
extract-password-required = Kata sandi diperlukan
extract-to = Ekstrak ke...
extract-to-title = Ekstrak ke map
empty-trash = Kosongkan sampah
empty-trash-title = Kosongkan sampah?
empty-trash-warning = Item di map Sampah akan dihapus permanen
emptying-trash = Mengosongkan { trash } ({ $progress })...
mount-error = Tidak dapat mengakses drive
create-new-file = Buat berkas baru
create-new-folder = Buat map baru
permanently-delete-question = Hapus secara permanen?
delete = Hapus
sort-by-trashed = Urutkan berdasarkan waktu penghapusan
delete-permanently = Hapus secara permanen
permanently-delete-warning = { $target } akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.
deleted =
{ $items } { $items ->
[one] item
*[other] item
} dihapus dari { trash }
permanently-deleted =
{ $items } { $items ->
[one] item
*[other] item
} dihapus secara permanen
file-name = Nama berkas
folder-name = Nama map
file-already-exists = Berkas dengan nama tersebut sudah ada
folder-already-exists = Map dengan nama tersebut sudah ada
name-hidden = Nama yang diawali dengan "." akan disembunyikan
name-invalid = Nama tidak boleh "{ $filename }"
name-no-slashes = Nama tidak boleh berisi garis miring
cancel = Batalkan
create = Buat
open = Buka
open-file = Buka berkas
open-folder = Buka map
open-in-new-tab = Buka di tab baru
open-in-new-window = Buka di jendela baru
open-item-location = Buka lokasi item
open-multiple-files = Buka beberapa berkas
open-multiple-folders = Buka beberapa map
save = Simpan
save-file = Simpan berkas
open-with-title = Bagaimana anda ingin membuka "{ $name }"?
browse-store = Telusuri { $store }
other-apps = Aplikasi lainnya
related-apps = Aplikasi terkait
rename-file = Ganti nama berkas
rename-folder = Ganti nama map
replace = Ganti
replace-title = "{ $filename }" sudah ada di lokasi ini
replace-warning-operation = Apakah anda ingin menggantinya? Menggantinya akan menimpa konten tersebut.
original-file = Berkas asli
replace-with = Ganti dengan
apply-to-all = Terapkan ke semua
replace-warning = Apakah anda ingin menggantinya dengan yang sedang anda simpan? Menggantinya akan menimpa konten tersebut.
keep-both = Pertahankan keduanya
skip = Lewati
set-executable-and-launch = Atur sebagai dijalankan dan luncurkan
set-and-launch = Atur dan luncurkan
set-executable-and-launch-description = Apakah anda ingin mengatur "{ $name }" sebagai dijalankan dan luncurkan?
open-with = Buka dengan
owner = Pemilik
group = Grup
other = Lainnya
none = Tidak ada
execute-only = Hanya jalankan
write-only = Hanya tulis
write-execute = Tulis dan jalankan
read-only = Hanya baca
read-execute = Baca dan jalankan
read-write = Baca dan tulis
read-write-execute = Baca, tulis, dan jalankan
favorite-path-error = Galat membuka direktori
remove = Hapus
keep = Pertahankan
repository = Repositori
favorite-path-error-description =
Tidak dapat membuka "{ $path }"
"{ $path }" mungkin tidak ada atau anda mungkin tidak memiliki izin untuk membuka
Apakah anda ingin menghapus dari bilah sisi?
support = Dukungan
add-network-drive = Tambahkan drive jaringan
connect = Sambungkan
connect-anonymously = Sambungkan secara anonim
connecting = Menyambungkan...
domain = Domain
enter-server-address = Masukkan alamat server
network-drive-description =
Alamat server mencakup awalan protokol dan alamat.
Contoh: ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-schemes =
Protokol yang tersedia,Awalan
AppleTalk,afp://
File Transfer Protocol,ftp:// atau ftps://
Network File System,nfs://
Server Message Block,smb://
SSH File Transfer Protocol,sftp:// atau ssh://
WebDAV,dav:// atau davs://
network-drive-error = Tidak dapat mengakses drive jaringan
password = Kata sandi
remember-password = Ingat kata sandi
try-again = Coba lagi
username = Nama pengguna
cancelled = Dibatalkan
edit-history = Sunting riwayat
history = Riwayat
no-history = Tidak ada item dalam riwayat.
pending = Menunggu
progress = { $percent }%
progress-cancelled = { $percent }%, dibatalkan
progress-failed = { $percent }%, gagal
progress-paused = { $percent }%, dijeda
failed = Gagal
complete = Selesai
copy_noun = Salin
creating = Membuat "{ $name }" di "{ $parent }"
created = "{ $name }" dibuat di "{ $parent }"
compressing =
Mengompres { $items } { $items ->
[one] item
*[other] item
} dari "{ $from }" ke "{ $to }" ({ $progress })...
compressed =
{ $items } { $items ->
[one] item
*[other] item
} dikompres dari "{ $from }" ke "{ $to }"
copied =
{ $items } { $items ->
[one] item
*[other] item
} disalin dari "{ $from }" ke "{ $to }"
copying =
Menyalin { $items } { $items ->
[one] item
*[other] item
} dari "{ $from }" ke "{ $to }" ({ $progress })...
deleting =
Menghapus { $items } { $items ->
[one] item
*[other] item
} dari { trash } ({ $progress })...
emptied-trash = { trash } telah dikosongkan
extracting =
Mengekstrak { $items } { $items ->
[one] item
*[other] item
} dari "{ $from }" ke "{ $to }" ({ $progress })...
extracted =
{ $items } { $items ->
[one] item
*[other] item
} diekstrak dari "{ $from }" ke "{ $to }"
setting-executable-and-launching = Mengatur "{ $name }" sebagai dijalankan dan meluncurkan
set-executable-and-launched = Atur "{ $name }" sebagai dijalankan dan diluncurkan
setting-permissions = Mengatur izin untuk "{ $name }" ke { $mode }
set-permissions = Atur izin untuk "{ $name }" ke { $mode }
menu-open-with = Buka dengan...
unknown-folder = map yang tidak diketahui
default-app = { $name } (bawaan)
show-details = Tampilkan rincian
type = Jenis: { $mime }
items = Item: { $items }
item-size = Ukuran: { $size }
moving =
Memindahkan { $items } { $items ->
[one] item
*[other] item
} dari "{ $from }" ke "{ $to }" ({ $progress })...
moved =
{ $items } { $items ->
[one] item
*[other] item
} dipindahkan dari "{ $from }" ke "{ $to }"
permanently-deleting =
Menghapus { $items } { $items ->
[one] item
*[other] item
} secara permanen
removing-from-recents =
Menghapus { $items } { $items ->
[one] item
*[other] item
} dari { recents }
removed-from-recents =
{ $items } { $items ->
[one] item
*[other] item
} dihapus dari { recents }
renaming = Mengganti nama "{ $from }" ke "{ $to }"
renamed = Nama diganti "{ $from }" ke "{ $to }"
restoring =
Memulihkan { $items } { $items ->
[one] item
*[other] item
} dari { trash } ({ $progress })...
restored =
{ $items } { $items ->
[one] item
*[other] item
} dipulihkan dari { trash }
item-created = Dibuat: { $created }
item-modified = Dimodifikasi: { $modified }
item-accessed = Diakses: { $accessed }
calculating = Menghitung...
settings = Pengaturan
single-click = Klik sekali untuk membuka
appearance = Tampilan
theme = Tema
match-desktop = Cocokkan desktop
dark = Gelap
light = Terang
type-to-search = Ketik untuk mencari
type-to-search-recursive = Mencari di map saat ini dan semua submap
type-to-search-enter-path = Memasukkan jalur ke direktori atau berkas
add-to-sidebar = Tambahkan ke bilah sisi
compress = Kompres
eject = Keluarkan
extract-here = Ekstrak
new-file = Berkas baru...
new-folder = Map baru...
open-in-terminal = Buka di terminal
move-to-trash = Pindahkan ke sampah
restore-from-trash = Pulihkan dari sampah
remove-from-sidebar = Hapus dari bilah sisi
sort-by-name = Urutkan berdasarkan nama
sort-by-modified = Urutkan berdasarkan dimodifikasi
sort-by-size = Urutkan berdasarkan ukuran
remove-from-recents = Hapus dari terbaru
change-wallpaper = Ubah wallpaper...
desktop-appearance = Tampilan desktop...
display-settings = Pengaturan layar...
file = Berkas
new-tab = Tab baru
new-window = Jendela baru
reload-folder = Muat ulang map
rename = Ganti nama...
close-tab = Tutup tab
quit = Keluar
edit = Sunting
cut = Potong
copy = Salin
paste = Tempel
select-all = Pilih semua
zoom-in = Perbesar
default-size = Ukuran bawaan
zoom-out = Perkecil
view = Tampilan
grid-view = Tampilan kisi
list-view = Tampilan daftar
gallery-preview = Tampilan galeri
show-hidden-files = Tampilkan berkas tersembunyi
list-directories-first = Daftar direktori terlebih dahulu
menu-settings = Pengaturan...
menu-about = Tentang Berkas COSMIC...
sort = Urutkan
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Terbaru terlebih dahulu
sort-oldest-first = Tertua terlebih dahulu
sort-smallest-to-largest = Terkecil hingga terbesar
sort-largest-to-smallest = Terbesar hingga terkecil
selected-items = { $items } item yang dipilih
type-to-search-select = Memilih berkas atau map pertama yang cocok

View file

@ -0,0 +1,306 @@
cosmic-files = COSMIC файлдары
empty-folder = Бос бума
empty-folder-hidden = Бос бума (жасырын элементтері бар)
no-results = Нәтижелер табылмады
filesystem = Файлдық жүйе
home = Үй
networks = Желілер
notification-in-progress = Файлдармен әрекеттер орындалуда
trash = Қоқыс шелегі
recents = Соңғылар
undo = Болдырмау
today = Бүгін
desktop-view-options = Жұмыс үстелінің көрініс опциялары...
show-on-desktop = Жұмыс үстелінде көрсету
desktop-folder-content = Жұмыс үстелі бумасының мазмұны
mounted-drives = Тіркелген дискілер
trash-folder-icon = Қоқыс шелегі бумасының таңбашасы
icon-size-and-spacing = Таңбаша өлшемі мен аралықтары
icon-size = Таңбаша өлшемі
grid-spacing = Тор аралықтары
name = Аты
modified = Өзгертілген
trashed-on = Қоқыс шелегіне тасталған
size = Өлшемі
details = Ақпараты
dismiss = Хабарламаны елемеу
operations-running =
{ $running } { $running ->
[one] әрекет
*[other] әрекет
} орындалуда ({ $percent }%)...
operations-running-finished =
{ $running } { $running ->
[one] әрекет
*[other] әрекет
} орындалуда ({ $percent }%), { $finished } аяқталды...
pause = Аялдату
resume = Жалғастыру
create-archive = Архив жасау
extract-password-required = Пароль керек
extract-to = Шығару...
extract-to-title = Бумаға шығару
empty-trash = Себетті тазарту
empty-trash-title = Себетті тазарту керек пе?
empty-trash-warning = Себет бумасындағы элементтер біржола өшіріледі
mount-error = Дискіге қол жеткізу мүмкін емес
create-new-file = Жаңа файл жасау
create-new-folder = Жаңа бума жасау
file-name = Файл аты
folder-name = Бума аты
file-already-exists = Ондай аты бар файл бұрыннан бар
folder-already-exists = Ондай аты бар бума бұрыннан бар
name-hidden = "." таңбасынан басталатын атаулар жасырын болады
name-invalid = Аты "{ $filename }" болуы мүмкін емес
name-no-slashes = Атауда қиғаш сызықтар болмауы тиіс
cancel = Бас тарту
create = Жасау
open = Ашу
open-file = Файлды ашу
open-folder = Буманы ашу
open-in-new-tab = Жаңа бетте ашу
open-in-new-window = Жаңа терезеде ашу
open-item-location = Нысанның орнын ашу
open-multiple-files = Бірнеше файлды ашу
open-multiple-folders = Бірнеше буманы ашу
save = Сақтау
save-file = Файлды сақтау
open-with-title = "{ $name }" қалай ашқыңыз келеді?
browse-store = { $store } шолу
other-apps = Басқа қолданбалар
related-apps = Қатысты қолданбалар
selected-items = Таңдалған { $items } нысан
permanently-delete-question = Біржола өшіру керек пе?
delete = Өшіру
permanently-delete-warning = { $target } біржола өшіріледі. Бұл әрекетті болдырмау мүмкін емес.
rename-file = Файлдың атын өзгерту
rename-folder = Буманың атын өзгерту
replace = Алмастыру
replace-title = Бұл жерде "{ $filename }" бұрыннан бар
replace-warning = Оны сақталып жатқан файлмен алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны қайта жазылады.
replace-warning-operation = Оны алмастыруды қалайсыз ба? Алмастыру кезінде оның мазмұны үстінен жазылады.
original-file = Түпнұсқа файл
replace-with = Келесімен алмастыру
apply-to-all = Барлығына іске асыру
keep-both = Екеуін де қалдыру
skip = Өткізіп жіберу
set-executable-and-launch = Орындалатын файл ретінде орнату және жөнелту
set-executable-and-launch-description = "{ $name }" нысанын орындалатын файл ретінде орнатып, оны жөнелтуді қалайсыз ба?
set-and-launch = Орнату және жөнелту
open-with = Көмегімен ашу
owner = Иесі
group = Топ
other = Басқа
none = Ештеңе
execute-only = Тек орындау
write-only = Тек жазу
write-execute = Жазу және орындау
read-only = Тек оқу
read-execute = Оқу және орындау
read-write = Оқу және жазу
read-write-execute = Оқу, жазу және орындау
favorite-path-error = Буманы ашу қатесі
favorite-path-error-description =
"{ $path }" ашу мүмкін емес
"{ $path }" жоқ болуы мүмкін немесе оны ашуға құқығыңыз жоқ
Оны бүйірлік панельден өшіруді қалайсыз ба?
remove = Өшіру
keep = Қалдыру
repository = Репозиторий
support = Қолдау
add-network-drive = Желілік дискіні қосу
connect = Байланысу
connect-anonymously = Анонимді түрде байланысу
connecting = Байланысуда...
domain = Домен
enter-server-address = Сервер адресін енгізіңіз
network-drive-description =
Сервер адрестері хаттама префиксі мен адрестен тұрады.
Мысалдар: ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-schemes =
Қолжетімді хаттамалар,Префикс
AppleTalk,afp://
Файлды тасымалдау хаттамасы,ftp:// немесе ftps://
Желілік файлдық жүйе,nfs://
Сервер хабарламаларының блогы,smb://
SSH файлды тасымалдау хаттамасы,sftp:// немесе ssh://
WebDAV,dav:// немесе davs://
network-drive-error = Желілік дискіге қол жеткізу мүмкін емес
password = Пароль
remember-password = Парольді есте сақтау
try-again = Қайтадан көру
username = Пайдаланушы аты
cancelled = Бас тартылды
edit-history = Тарихты түзету
history = Тарихы
no-history = Тарихта ешқандай элемент жоқ.
pending = Күтілуде
progress = { $percent }%
progress-cancelled = { $percent }%, бас тартылды
progress-failed = { $percent }%, сәтсіз аяқталды
progress-paused = { $percent }%, аялдатылды
failed = Сәтсіз аяқталды
complete = Аяқталды
compressing =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} "{ $from }" ішінен "{ $to }" ішіне сығу ({ $progress })...
compressed =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} "{ $from }" ішінен "{ $to }" ішіне сығылды
copy_noun = Көшіріп алу
creating = "{ $parent }" ішінде "{ $name }" жасау
created = "{ $parent }" ішінде "{ $name }" жасалды
copying =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} "{ $from }" ішінен "{ $to }" ішіне көшіру ({ $progress })...
copied =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} "{ $from }" ішінен "{ $to }" ішіне көшірілді
deleting =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} { trash } ішінен өшіру ({ $progress })...
deleted =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} { trash } ішінен өшірілді
emptying-trash = { trash } тазартылуда ({ $progress })...
emptied-trash = { trash } тазартылды
extracting =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} "{ $from }" ішінен "{ $to }" ішіне тарқату ({ $progress })...
extracted =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} "{ $from }" ішінен "{ $to }" ішіне тарқатылды
setting-executable-and-launching = "{ $name }" орындалатын файл ретінде орнату және іске қосу
set-executable-and-launched = "{ $name }" орындалатын файл ретінде орнатылды және іске қосылды
setting-permissions = "{ $name }" үшін рұқсаттарды { $mode } мәніне орнату
set-permissions = "{ $name }" үшін рұқсаттар { $mode } мәніне орнатылды
moving =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} "{ $from }" ішінен "{ $to }" ішіне жылжыту ({ $progress })...
moved =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} "{ $from }" ішінен "{ $to }" ішіне жылжытылды
permanently-deleting =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} біржола өшіру
permanently-deleted =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} біржола өшірілді
removing-from-recents =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} { recents } тізімінен өшіру
removed-from-recents =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} { recents } тізімінен өшірілді
renaming = "{ $from }" атын "{ $to }" деп өзгерту
renamed = "{ $from }" аты "{ $to }" деп өзгертілді
restoring =
{ $items } { $items ->
[one] нәрсені
*[other] нәрсені
} { trash } ішінен қалпына келтіру ({ $progress })...
restored =
{ $items } { $items ->
[one] нәрсе
*[other] нәрсе
} { trash } ішінен қалпына келтірілді
unknown-folder = белгісіз бума
menu-open-with = Көмегімен ашу...
default-app = { $name } (әдепкі)
show-details = Мәліметтерді көрсету
type = Түрі: { $mime }
items = Элементтер: { $items }
item-size = Өлшемі: { $size }
item-created = Жасалған: { $created }
item-modified = Өзгертілген: { $modified }
item-accessed = Қол жеткізілген: { $accessed }
calculating = Есептеу...
settings = Баптаулар
single-click = Ашу үшін бір рет шерту
appearance = Сыртқы түрі
theme = Тақырып
match-desktop = Жұмыс үстеліне сәйкес келу
dark = Күңгірт
light = Ашық
type-to-search = Іздеу үшін теру
type-to-search-recursive = Ағымдағы бума мен барлық ішкі бумаларды іздейді
type-to-search-enter-path = Бумаға немесе файлға жолды енгізеді
type-to-search-select = Бірінші сәйкес келетін файлды немесе буманы таңдайды
add-to-sidebar = Бүйірлік панельге қосу
compress = Сығу
delete-permanently = Біржолата өшіру
eject = Шығару
extract-here = Тарқату
new-file = Жаңа файл...
new-folder = Жаңа бума...
open-in-terminal = Терминалда ашу
move-to-trash = Қоқыс жәшігіне тастау
restore-from-trash = Қоқыс жәшігінен қалпына келтіру
remove-from-sidebar = Бүйірлік панельден өшіру
sort-by-name = Аты бойынша сұрыптау
sort-by-modified = Өзгертілген уақыты бойынша сұрыптау
sort-by-size = Өлшемі бойынша сұрыптау
sort-by-trashed = Өшірілген уақыты бойынша сұрыптау
remove-from-recents = Соңғылардан өшіру
change-wallpaper = Тұсқағазды өзгерту...
desktop-appearance = Жұмыс үстелінің сыртқы түрі...
display-settings = Көрсету баптаулары...
file = Файл
new-tab = Жаңа бет
new-window = Жаңа терезе
reload-folder = Буманы қайта жүктеу
rename = Атын өзгерту...
close-tab = Бетті жабу
quit = Шығу
edit = Түзету
cut = Қиып алу
copy = Көшіру
paste = Кірістіру
select-all = Барлығын таңдау
zoom-in = Үлкейту
default-size = Әдепкі өлшем
zoom-out = Кішірейту
view = Көрініс
grid-view = Тор көрінісі
list-view = Тізім көрінісі
show-hidden-files = Жасырын файлдарды көрсету
list-directories-first = Алдымен бумаларды тізімдеу
gallery-preview = Галереяны алдын ала қарау
menu-settings = Баптаулар...
menu-about = COSMIC файлдар туралы...
sort = Сұрыптау
sort-a-z = А-Я
sort-z-a = Я-А
sort-newest-first = Алдымен жаңалары
sort-oldest-first = Алдымен ескілері
sort-smallest-to-largest = Кішісінен үлкеніне
sort-largest-to-smallest = Үлкенінен кішісіне

View file

@ -68,7 +68,7 @@ dark = 다크
light = 라이트 light = 라이트
# Context menu # Context menu
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 = 휴지통에서 복구
@ -84,7 +84,7 @@ sort-by-size = 크기 순으로 정렬
file = 파일 file = 파일
new-tab = 새 탭 new-tab = 새 탭
new-window = 새 창 new-window = 새 창
rename = 이름 바꾸기 rename = 이름 바꾸기...
close-tab = 탭 닫기 close-tab = 탭 닫기
quit = 종료 quit = 종료
@ -102,7 +102,7 @@ view = 보기
grid-view = 그리드 보기 grid-view = 그리드 보기
list-view = 목록 보기 list-view = 목록 보기
menu-settings = 설정... menu-settings = 설정...
menu-about = 코스믹 파일에 대하여... menu-about = COSMIC 파일 정보...
connect = 연결 connect = 연결
read-execute = 읽기 및 실행 read-execute = 읽기 및 실행
item-modified = 마지막 수정 일자: { $modified } item-modified = 마지막 수정 일자: { $modified }
@ -171,7 +171,7 @@ details = 세부 정보
mounted-drives = 마운트된 드라이브 mounted-drives = 마운트된 드라이브
mount-error = 드라이브에 접근할 수 없음 mount-error = 드라이브에 접근할 수 없음
extract-here = 압축 해제 extract-here = 압축 해제
removed-from-recents = { recents }에서 { $items } 항목 제거됨 removed-from-recents = { recents } 에서 { $items }개의 항목을 제거했습니다
add-to-sidebar = 사이드 바에 추가 add-to-sidebar = 사이드 바에 추가
item-created = 생성 일자: { $created } item-created = 생성 일자: { $created }
type-to-search-recursive = 현재 폴더와 하위 폴더 탐색 type-to-search-recursive = 현재 폴더와 하위 폴더 탐색
@ -202,7 +202,7 @@ delete-permanently = 완전히 삭제
networks = 네트워크 networks = 네트워크
write-only = 쓰기 전용 write-only = 쓰기 전용
today = 오늘 today = 오늘
permanently-delete-warning = { $target }이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다. permanently-delete-warning = { $target } 이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다.
empty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다 empty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다
empty-trash = 휴지통 비우기 empty-trash = 휴지통 비우기
empty-trash-title = 휴지통을 비울까요? empty-trash-title = 휴지통을 비울까요?
@ -210,3 +210,64 @@ type-to-search = 입력하여 검색
notification-in-progress = 파일 작업이 진행 중입니다 notification-in-progress = 파일 작업이 진행 중입니다
permanently-delete-question = 완전히 삭제할까요? permanently-delete-question = 완전히 삭제할까요?
selected-items = { $items }개 항목 선택됨 selected-items = { $items }개 항목 선택됨
sort-newest-first = 새 항목 우선
renamed = "{ $from }" 에서 "{ $to }" 로 이름 변경됨
deleted = { trash } 에서 { $items }개의 항목을 제거했습니다
reload-folder = 폴더 새로고침
favorite-path-error = 디렉터리를 여는 중 오류가 발생했습니다
remove-from-sidebar = 사이드 바에서 제거
restoring = { trash } 에서 { $items }개의 항목을 복구 중 ({ $progress })...
gallery-preview = 갤러리 미리보기
sort-smallest-to-largest = 작은 항목부터 큰 항목
zoom-in = 확대
removing-from-recents = { recents } 에서 { $items }개의 항목을 제거 중
zoom-out = 축소
compressing = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 중({ $progress })...
setting-executable-and-launching = "{ $name }"를 실행 가능으로 설정 및 실행 중
default-size = 기본 크기
extracted = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 해제했습니다
permanently-deleting = { $items }개의 항목을 영구적으로 제거 중
compressed = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축했습니다
grid-spacing = 그리드 간격
copying = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 복사 중({ $progress })...
sort-oldest-first = 오래된 항목 우선
sort-by-trashed = 삭제된 시간 순으로 정렬
copied = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 복사했습니다
list-directories-first = 폴더 우선 나열
remove-from-recents = 최근 항목에서 제거
moving = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 이동 중 ({ $progress })...
change-wallpaper = 배경화면 변경...
deleting = { trash } 에서 { $items }개의 항목을 제거 중({ $progress })...
set-executable-and-launched = "{ $name }"를 실행 가능으로 설정 및 실행됨
sort-a-z = A-Z
set-and-launch = 설정 후 실행
set-executable-and-launch = 실행 가능으로 설정 후 실행
restored = { trash } 에서 { $items }개의 항목을 복구했습니다
sort-z-a = Z-A
operations-running-finished = { $running }개의 작업 진행 중 ({ $percent }%), { $finished } 완료됨...
sort = 정렬
show-hidden-files = 숨긴 파일 표시
trash-folder-icon = 휴지통 아이콘
extracting = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 압축 해제 중 ({ $progress })...
permanently-deleted = { $items }개의 항목을 영구적으로 제거했습니다
renaming = "{ $from }" 에서 "{ $to }" 로 이름 변경 중
set-executable-and-launch-description = "{ $name }"을 실행 가능으로 설정하고 실행할까요?
sort-largest-to-smallest = 큰 항목부터 작은 항목
moved = "{ $from }"에서 "{ $to }"(으)로 { $items }개의 항목을 이동했습니다
display-settings = 화면 설정...
desktop-appearance = 데스크톱 외관...
favorite-path-error-description =
"{ $path }"을(를) 열 수 없습니다
"{ $path }"이(가) 존재하지 않거나 열기 권한이 없을 수 있습니다
사이드바에서 제거하시겠습니까?
operations-running = { $running }개의 작업 진행 중 ({ $percent }%)...
network-drive-schemes =
지원 프로토콜,접두사(Prefix)
AppleTalk,afp://
파일 전송 프로토콜 (FTP),ftp:// 또는 ftps://
네트워크 파일 시스템 (NFS),nfs://
서버 메시지 블록 (SMB),smb://
SSH 파일 전송 프로토콜 (SFTP),sftp:// 또는 ssh://
WebDAV,dav:// 또는 davs://
type-to-search-select = 일치하는 첫 번째 파일 또는 폴더를 선택합니다

0
i18n/ms/cosmic_files.ftl Normal file
View file

View file

@ -3,8 +3,8 @@ empty-folder = Lege map
empty-folder-hidden = Lege map (met verborgen bestanden) empty-folder-hidden = Lege map (met verborgen bestanden)
no-results = Geen resultaten gevonden no-results = Geen resultaten gevonden
filesystem = Bestandssysteem filesystem = Bestandssysteem
home = Home home = Persoonlijke map
networks = Netwerk networks = Netwerken
notification-in-progress = Bestanden worden nog bewerkt notification-in-progress = Bestanden worden nog bewerkt
trash = Prullenbak trash = Prullenbak
recents = Recente bestanden recents = Recente bestanden

View file

@ -16,8 +16,8 @@ size = Розмір
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = Спорожнити Смітник empty-trash = Спорожнити смітник
empty-trash-warning = Ви впевнені, що хочете остаточно видалити всі елементи зі Смітника? empty-trash-warning = Елементи зі смітника будуть остаточно видалені
## New File/Folder Dialog ## New File/Folder Dialog
@ -25,11 +25,11 @@ 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
@ -52,7 +52,7 @@ 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 = Початковий файл
@ -185,7 +185,7 @@ remove = Вилучити
cancelled = Скасовані cancelled = Скасовані
no-results = Нічого не знайдено no-results = Нічого не знайдено
networks = Мережі networks = Мережі
notification-in-progress = Виконуються операції з файлами. notification-in-progress = Триває обробка файлів
today = Сьогодні today = Сьогодні
desktop-view-options = Параметри вигляду стільниці... desktop-view-options = Параметри вигляду стільниці...
show-on-desktop = Показувати на стільниці show-on-desktop = Показувати на стільниці
@ -219,9 +219,9 @@ open-with-title = Як ви бажаєте відкрити "{ $name }"?
browse-store = Переглянути { $store } browse-store = Переглянути { $store }
other-apps = Інші застосунки other-apps = Інші застосунки
related-apps = Пов'язані застосунки related-apps = Пов'язані застосунки
permanently-delete-question = Вилучити остаточно permanently-delete-question = Остаточно видалити?
delete = Вилучити delete = Вилучити
permanently-delete-warning = Ви впевнені, що хочете остаточно вилучити { $target }? Дію неможливо скасувати. permanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати.
set-executable-and-launch = Зробити виконуваним і запустити set-executable-and-launch = Зробити виконуваним і запустити
set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його? set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його?
set-and-launch = Зробити і запустити set-and-launch = Зробити і запустити
@ -239,8 +239,8 @@ read-write = Перегляд і запис
read-write-execute = Перегляд, запис і виконання read-write-execute = Перегляд, запис і виконання
favorite-path-error = Помилка при відкритті каталогу favorite-path-error = Помилка при відкритті каталогу
favorite-path-error-description = favorite-path-error-description =
Неможливо відкрити "{ $path }". Неможливо відкрити "{ $path }"
Можливо, його не існує або у вас немає прав на відкриття. "{ $path }" можливо, його не існує або у вас немає прав на відкриття
Вилучити з бічної панелі? Вилучити з бічної панелі?
keep = Залишити keep = Залишити
@ -302,7 +302,7 @@ extracted =
} з "{ $from }" до "{ $to }" } з "{ $from }" до "{ $to }"
setting-executable-and-launching = Встановлення "{ $name }" виконуваним і запуск setting-executable-and-launching = Встановлення "{ $name }" виконуваним і запуск
set-executable-and-launched = Встановлено "{ $name }" виконуваним і запущено set-executable-and-launched = Встановлено "{ $name }" виконуваним і запущено
selected-items = { $items } обраних елементів selected-items = Вибрані { $items } елементи
setting-permissions = Встановлення дозволів { $mode } для "{ $name }" setting-permissions = Встановлення дозволів { $mode } для "{ $name }"
set-permissions = Встановлено дозволи { $mode } для "{ $name }" set-permissions = Встановлено дозволи { $mode } для "{ $name }"
show-details = Показати деталі show-details = Показати деталі
@ -355,3 +355,5 @@ removed-from-recents =
[one] елемент [one] елемент
*[other] елементи *[other] елементи
} з { recents } } з { recents }
empty-trash-title = Спорожити смітник?
type-to-search-select = Вибирає перший відповідний файл або папку

0
i18n/uz/cosmic_files.ftl Normal file
View file

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::{ iced::{
self, Alignment, Event, Length, Rectangle, Size, Subscription, self, Alignment, Event, Length, Rectangle, Size, Subscription,
clipboard::dnd::DndAction, clipboard::dnd::DndAction,
@ -39,7 +41,7 @@ use cosmic::{
dnd_destination::DragId, dnd_destination::DragId,
horizontal_space, icon, horizontal_space, icon,
menu::{action::MenuAction, key_bind::KeyBind}, menu::{action::MenuAction, key_bind::KeyBind},
segmented_button::{self, Entity}, segmented_button::{self, Entity, ReorderEvent},
vertical_space, vertical_space,
}, },
}; };
@ -72,10 +74,10 @@ use crate::{
FxOrderMap, FxOrderMap,
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{ config::{
AppTheme, Config, DesktopConfig, Favorite, IconSizes, TIME_CONFIG_ID, TabConfig, AppTheme, Config, DesktopConfig, Favorite, IconSizes, State, TIME_CONFIG_ID, TabConfig,
TimeConfig, TypeToSearch, TimeConfig, TypeToSearch,
}, },
dialog::{Dialog, DialogKind, DialogMessage, DialogResult}, dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings},
fl, home_dir, fl, home_dir,
key_bind::key_binds, key_bind::key_binds,
localize::LANGUAGE_SORTER, localize::LANGUAGE_SORTER,
@ -91,10 +93,6 @@ use crate::{
tab::{ tab::{
self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, Tab, self, HOVER_DURATION, HeadingOptions, ItemMetadata, Location, SORT_OPTION_FALLBACK, Tab,
}, },
};
use crate::{
config::State,
dialog::DialogSettings,
zoom::{zoom_in_view, zoom_out_view, zoom_to_default}, zoom::{zoom_in_view, zoom_out_view, zoom_to_default},
}; };
@ -396,6 +394,7 @@ pub enum Message {
PendingPauseAll(bool), PendingPauseAll(bool),
PermanentlyDelete(Option<Entity>), PermanentlyDelete(Option<Entity>),
Preview(Option<Entity>), Preview(Option<Entity>),
ReorderTab(ReorderEvent),
RescanRecents, RescanRecents,
RescanTrash, RescanTrash,
RemoveFromRecents(Option<Entity>), RemoveFromRecents(Option<Entity>),
@ -444,7 +443,7 @@ pub enum Message {
DndHoverTabTimeout(Entity), DndHoverTabTimeout(Entity),
DndEnterNav(Entity), DndEnterNav(Entity),
DndExitNav, DndExitNav,
DndEnterTab(Entity), DndEnterTab(Entity, Vec<String>),
DndExitTab, DndExitTab,
DndDropTab(Entity, Option<ClipboardPaste>, DndAction), DndDropTab(Entity, Option<ClipboardPaste>, DndAction),
DndDropNav(Entity, Option<ClipboardPaste>, DndAction), DndDropNav(Entity, Option<ClipboardPaste>, DndAction),
@ -845,8 +844,8 @@ impl App {
fn launch_desktop_entries(paths: &[impl AsRef<Path>]) { fn launch_desktop_entries(paths: &[impl AsRef<Path>]) {
for path in paths.iter().map(AsRef::as_ref) { for path in paths.iter().map(AsRef::as_ref) {
match freedesktop_entry_parser::parse_entry(path) { match DesktopEntry::from_path::<&str>(path, None) {
Ok(entry) => match entry.section("Desktop Entry").attr("Exec") { Ok(entry) => match entry.exec() {
Some(exec) => match mime_app::exec_to_command(exec, &[] as &[&str; 0]) { Some(exec) => match mime_app::exec_to_command(exec, &[] as &[&str; 0]) {
Some(commands) => { Some(commands) => {
for mut command in commands { for mut command in commands {
@ -1229,12 +1228,12 @@ impl App {
}; };
for item in items { for item in items {
if item.selected { if item.selected {
if let Some(path) = item.path_opt() { if let Some(path) = item.path_opt()
if op_sel.selected.contains(path) || op_sel.ignored.contains(path) { && (op_sel.selected.contains(path) || op_sel.ignored.contains(path))
{
// Ignore if path in selected or ignored paths // Ignore if path in selected or ignored paths
continue; continue;
} }
}
// Return if there is a previous selection not matching // Return if there is a previous selection not matching
return Task::none(); return Task::none();
@ -1890,13 +1889,13 @@ impl App {
children.push(item.preview_view(Some(&self.mime_app_cache), military_time)); children.push(item.preview_view(Some(&self.mime_app_cache), military_time));
} }
PreviewKind::Location(location) => { PreviewKind::Location(location) => {
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity)
if let Some(items) = tab.items_opt() { && let Some(items) = tab.items_opt()
{
for item in items { for item in items {
if item.location_opt.as_ref() == Some(location) { if item.location_opt.as_ref() == Some(location) {
children.push( children
item.preview_view(Some(&self.mime_app_cache), military_time), .push(item.preview_view(Some(&self.mime_app_cache), military_time));
);
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1904,10 +1903,10 @@ impl App {
} }
} }
} }
}
PreviewKind::Selected => { PreviewKind::Selected => {
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity)
if let Some(items) = tab.items_opt() { && let Some(items) = tab.items_opt()
{
let preview_opt = { let preview_opt = {
let mut selected = items.iter().filter(|item| item.selected); let mut selected = items.iter().filter(|item| item.selected);
@ -1915,9 +1914,9 @@ impl App {
// At least two selected items // At least two selected items
(Some(_), Some(_)) => Some(tab.multi_preview_view()), (Some(_), Some(_)) => Some(tab.multi_preview_view()),
// Exactly one selected item // Exactly one selected item
(Some(item), None) => Some( (Some(item), None) => {
item.preview_view(Some(&self.mime_app_cache), military_time), Some(item.preview_view(Some(&self.mime_app_cache), military_time))
), }
// No selected items // No selected items
_ => None, _ => None,
} }
@ -1927,13 +1926,10 @@ impl App {
children.push(preview); children.push(preview);
} }
if children.is_empty() { if children.is_empty()
if let Some(item) = &tab.parent_item_opt { && let Some(item) = &tab.parent_item_opt
children.push( {
item.preview_view(Some(&self.mime_app_cache), military_time), children.push(item.preview_view(Some(&self.mime_app_cache), military_time));
);
}
}
} }
} }
} }
@ -2061,14 +2057,14 @@ impl App {
.map(|favorite| { .map(|favorite| {
if let Favorite::Path(path) = favorite { if let Favorite::Path(path) = favorite {
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) {
if path.starts_with(from) { if path.starts_with(from)
if let Ok(relative) = path.strip_prefix(from) { && let Ok(relative) = path.strip_prefix(from)
{
favorites_changed = true; favorites_changed = true;
return Favorite::from_path(to.join(relative)); return Favorite::from_path(to.join(relative));
} }
} }
} }
}
favorite.clone() favorite.clone()
}) })
.collect(); .collect();
@ -2234,9 +2230,10 @@ impl Application for App {
let mut commands = vec![app.update_config()]; let mut commands = vec![app.update_config()];
for location in flags.locations { for location in flags.locations {
if let Some(path) = location.path_opt() { if let Some(path) = location.path_opt()
if path.is_file() { && path.is_file()
if let Some(parent) = path.parent() { && let Some(parent) = path.parent()
{
commands.push(app.open_tab( commands.push(app.open_tab(
Location::Path(parent.to_path_buf()), Location::Path(parent.to_path_buf()),
true, true,
@ -2244,8 +2241,6 @@ impl Application for App {
)); ));
continue; continue;
} }
}
}
commands.push(app.open_tab(location, true, None)); commands.push(app.open_tab(location, true, None));
} }
for location in flags.uris { for location in flags.uris {
@ -2404,15 +2399,14 @@ impl Application for App {
// TODO do we need to choose the correct mounter? // TODO do we need to choose the correct mounter?
self.mounter_items.keys().copied().next() self.mounter_items.keys().copied().next()
}) })
&& let Some(mounter) = MOUNTERS.get(&key)
{ {
if let Some(mounter) = MOUNTERS.get(&key) {
return mounter.network_drive(uri.clone()).map(move |()| { return mounter.network_drive(uri.clone()).map(move |()| {
cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount { cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount {
entity, entity,
}) })
}); });
} }
}
log::warn!( log::warn!(
"failed to open favorite, path does not exist: {}", "failed to open favorite, path does not exist: {}",
@ -2467,13 +2461,13 @@ impl Application for App {
return self.update(message); return self.update(message);
} }
} }
if let Some(data) = self.nav_model.data::<MounterData>(entity) { if let Some(data) = self.nav_model.data::<MounterData>(entity)
if let Some(mounter) = MOUNTERS.get(&data.0) { && let Some(mounter) = MOUNTERS.get(&data.0)
{
return mounter return mounter
.mount(data.1.clone()) .mount(data.1.clone())
.map(|()| cosmic::action::none()); .map(|()| cosmic::action::none());
} }
}
Task::none() Task::none()
} }
@ -2504,12 +2498,12 @@ impl Application for App {
} }
// Close gallery mode if open // Close gallery mode if open
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if tab.gallery { && tab.gallery
{
tab.gallery = false; tab.gallery = false;
return Task::none(); return Task::none();
} }
}
// Close menus and context panes in order per message // Close menus and context panes in order per message
// Why: It'd be weird to close everything all at once // Why: It'd be weird to close everything all at once
@ -2634,8 +2628,9 @@ impl Application for App {
} }
Message::Compress(entity_opt) => { Message::Compress(entity_opt) => {
let paths: Box<[_]> = self.selected_paths(entity_opt).collect(); let paths: Box<[_]> = self.selected_paths(entity_opt).collect();
if let Some(current_path) = paths.first() { if let Some(current_path) = paths.first()
if let Some(destination) = current_path.parent().zip(current_path.file_stem()) { && let Some(destination) = current_path.parent().zip(current_path.file_stem())
{
let to = destination.0.to_path_buf(); let to = destination.0.to_path_buf();
let name = destination.1.to_str().unwrap_or_default().to_string(); let name = destination.1.to_str().unwrap_or_default().to_string();
let archive_type = ArchiveType::default(); let archive_type = ArchiveType::default();
@ -2651,7 +2646,6 @@ impl Application for App {
); );
} }
} }
}
Message::Config(config) => { Message::Config(config) => {
if config != self.config { if config != self.config {
log::info!("update config"); log::info!("update config");
@ -2663,11 +2657,11 @@ impl Application for App {
} }
} }
Message::Copy(entity_opt) => { Message::Copy(entity_opt) => {
if let Some(entity) = entity_opt { if let Some(entity) = entity_opt
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { && let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
{
tab.refresh_cut(&[]); tab.refresh_cut(&[]);
} }
}
let paths = self.selected_paths(entity_opt); let paths = self.selected_paths(entity_opt);
let contents = ClipboardCopy::new(ClipboardKind::Copy, paths); let contents = ClipboardCopy::new(ClipboardKind::Copy, paths);
return clipboard::write_data(contents); return clipboard::write_data(contents);
@ -2974,15 +2968,15 @@ impl Application for App {
DialogResult::Cancel => {} DialogResult::Cancel => {}
DialogResult::Open(selected_paths) => { DialogResult::Open(selected_paths) => {
let mut archive_paths = None; let mut archive_paths = None;
if let Some(file_dialog) = &self.file_dialog_opt { if let Some(file_dialog) = &self.file_dialog_opt
if let Some(window) = self.windows.remove(&file_dialog.window_id()) { && let Some(window) = self.windows.remove(&file_dialog.window_id())
if let WindowKind::FileDialog(paths) = window.kind { && let WindowKind::FileDialog(paths) = window.kind
{
archive_paths = paths; archive_paths = paths;
} }
} if let Some(archive_paths) = archive_paths
} && !selected_paths.is_empty()
if let Some(archive_paths) = archive_paths { {
if !selected_paths.is_empty() {
self.file_dialog_opt = None; self.file_dialog_opt = None;
return self.operation(Operation::Extract { return self.operation(Operation::Extract {
paths: archive_paths, paths: archive_paths,
@ -2992,7 +2986,6 @@ impl Application for App {
} }
} }
} }
}
self.file_dialog_opt = None; self.file_dialog_opt = None;
} }
Message::FileDialogMessage(dialog_message) => { Message::FileDialogMessage(dialog_message) => {
@ -3019,12 +3012,11 @@ impl Application for App {
&& !modifiers.control() && !modifiers.control()
&& !modifiers.alt() && !modifiers.alt()
&& matches!(key, Key::Character(_)) && matches!(key, Key::Character(_))
&& let Some(text) = text
{ {
if let Some(text) = text {
match self.config.type_to_search { match self.config.type_to_search {
TypeToSearch::Recursive => { TypeToSearch::Recursive => {
let mut term = let mut term = self.search_get().unwrap_or_default().to_string();
self.search_get().unwrap_or_default().to_string();
term.push_str(&text); term.push_str(&text);
return self.search_set_active(Some(term)); return self.search_set_active(Some(term));
} }
@ -3041,8 +3033,7 @@ impl Application for App {
tab.edit_location = tab.edit_location =
Some(location.with_uri(uri_string).into()); Some(location.with_uri(uri_string).into());
} else if let Some(path) = location.path_opt() { } else if let Some(path) = location.path_opt() {
let mut path_string = let mut path_string = path.to_string_lossy().into_owned();
path.to_string_lossy().into_owned();
path_string.push_str(&text); path_string.push_str(&text);
tab.edit_location = tab.edit_location =
Some(location.with_path(path_string.into()).into()); Some(location.with_path(path_string.into()).into());
@ -3051,11 +3042,11 @@ impl Application for App {
} }
TypeToSearch::SelectByPrefix => { TypeToSearch::SelectByPrefix => {
// Reset buffer if timeout elapsed // Reset buffer if timeout elapsed
if let Some(last_key) = self.type_select_last_key { if let Some(last_key) = self.type_select_last_key
if last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT
{
self.type_select_prefix.clear(); self.type_select_prefix.clear();
} }
}
// Accumulate character and select // Accumulate character and select
self.type_select_prefix.push_str(&text.to_lowercase()); self.type_select_prefix.push_str(&text.to_lowercase());
@ -3075,7 +3066,6 @@ impl Application for App {
} }
} }
} }
}
Message::MaybeExit => { Message::MaybeExit => {
if self.core.main_window_id().is_none() && self.pending_operations.is_empty() { if self.core.main_window_id().is_none() && self.pending_operations.is_empty() {
// Exit if window is closed and there are no pending operations // Exit if window is closed and there are no pending operations
@ -3105,24 +3095,25 @@ impl Application for App {
let mut unmounted = Vec::new(); let mut unmounted = Vec::new();
if let Some(old_items) = self.mounter_items.get(&mounter_key) { if let Some(old_items) = self.mounter_items.get(&mounter_key) {
for old_item in old_items { for old_item in old_items {
if let Some(old_path) = old_item.path() { if let Some(old_path) = old_item.path()
if old_item.is_mounted() { && old_item.is_mounted()
{
let mut still_mounted = false; let mut still_mounted = false;
for item in &mounter_items { for item in &mounter_items {
if let Some(path) = item.path() { if let Some(path) = item.path()
if path == old_path && item.is_mounted() { && path == old_path
&& item.is_mounted()
{
still_mounted = true; still_mounted = true;
break; break;
} }
} }
}
if !still_mounted { if !still_mounted {
unmounted.push(old_path); unmounted.push(old_path);
} }
} }
} }
} }
}
// Go back to home in any tabs that were unmounted // Go back to home in any tabs that were unmounted
let mut commands = Vec::new(); let mut commands = Vec::new();
@ -3253,8 +3244,9 @@ impl Application for App {
} }
Message::NewItem(entity_opt, dir) => { Message::NewItem(entity_opt, dir) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(path) = tab.location.path_opt() { && let Some(path) = tab.location.path_opt()
{
return Task::batch([ return Task::batch([
self.dialog_pages.push_back(DialogPage::NewItem { self.dialog_pages.push_back(DialogPage::NewItem {
parent: path.clone(), parent: path.clone(),
@ -3265,7 +3257,6 @@ impl Application for App {
]); ]);
} }
} }
}
#[cfg(feature = "notify")] #[cfg(feature = "notify")]
Message::Notification(notification) => { Message::Notification(notification) => {
self.notification_opt = Some(notification); self.notification_opt = Some(notification);
@ -3276,8 +3267,9 @@ impl Application for App {
let mut needs_reload = Vec::new(); let mut needs_reload = Vec::new();
let entities: Box<[_]> = self.tab_model.iter().collect(); let entities: Box<[_]> = self.tab_model.iter().collect();
for entity in entities { for entity in entities {
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(path) = tab.location.path_opt() { && let Some(path) = tab.location.path_opt()
{
let mut contains_change = false; let mut contains_change = false;
for event in &events { for event in &events {
for event_path in &event.paths { for event_path in &event.paths {
@ -3329,7 +3321,6 @@ impl Application for App {
} }
} }
} }
}
let commands = needs_reload let commands = needs_reload
.into_iter() .into_iter()
@ -3350,22 +3341,23 @@ impl Application for App {
if let Some(terminal) = self.mime_app_cache.terminal() { if let Some(terminal) = self.mime_app_cache.terminal() {
let mut paths = Box::from([]); let mut paths = Box::from([]);
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(path) = tab.location.path_opt() { && let Some(path) = tab.location.path_opt()
{
if let Some(items) = tab.items_opt() { if let Some(items) = tab.items_opt() {
paths = paths = items
items
.iter() .iter()
.filter_map(|item| { .filter_map(
|item| {
if item.selected { item.path_opt() } else { None } if item.selected { item.path_opt() } else { None }
}) },
)
.collect(); .collect();
} }
if paths.is_empty() { if paths.is_empty() {
paths = Box::from([path]); paths = Box::from([path]);
} }
} }
}
for path in paths { for path in paths {
if let Some(mut command) = terminal if let Some(mut command) = terminal
.command::<&str>(&[]) .command::<&str>(&[])
@ -3453,8 +3445,9 @@ impl Application for App {
}, },
Message::OpenWithDialog(entity_opt) => { Message::OpenWithDialog(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity)
if let Some(items) = tab.items_opt() { && let Some(items) = tab.items_opt()
{
for item in items { for item in items {
if !item.selected { if !item.selected {
continue; continue;
@ -3479,7 +3472,6 @@ impl Application for App {
} }
} }
} }
}
Message::OpenWithSelection(index) => { Message::OpenWithSelection(index) => {
if let Some(DialogPage::OpenWith { selected, .. }) = self.dialog_pages.front_mut() { if let Some(DialogPage::OpenWith { selected, .. }) = self.dialog_pages.front_mut() {
*selected = index; *selected = index;
@ -3487,21 +3479,20 @@ impl Application for App {
} }
Message::Paste(entity_opt) => { Message::Paste(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(path) = tab.location.path_opt() { && let Some(path) = tab.location.path_opt()
{
let to = path.clone(); let to = path.clone();
return clipboard::read_data::<ClipboardPaste>().map(move |contents_opt| { return clipboard::read_data::<ClipboardPaste>().map(move |contents_opt| {
match contents_opt { match contents_opt {
Some(contents) => cosmic::action::app(Message::PasteContents( Some(contents) => {
to.clone(), cosmic::action::app(Message::PasteContents(to.clone(), contents))
contents, }
)),
None => cosmic::action::none(), None => cosmic::action::none(),
} }
}); });
} }
} }
}
Message::PasteContents(to, mut contents) => { Message::PasteContents(to, mut contents) => {
contents.paths.retain(|p| *p != to); contents.paths.retain(|p| *p != to);
if !contents.paths.is_empty() { if !contents.paths.is_empty() {
@ -3732,8 +3723,9 @@ impl Application for App {
} }
Message::Rename(entity_opt) => { Message::Rename(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(items) = tab.items_opt() { && let Some(items) = tab.items_opt()
{
let selected: Box<[_]> = items let selected: Box<[_]> = items
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
@ -3766,7 +3758,6 @@ impl Application for App {
} }
} }
} }
}
Message::ReplaceResult(replace_result) => { Message::ReplaceResult(replace_result) => {
if let Some((dialog_page, task)) = self.dialog_pages.pop_front() { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() {
match dialog_page { match dialog_page {
@ -3786,8 +3777,9 @@ impl Application for App {
Message::RestoreFromTrash(entity_opt) => { Message::RestoreFromTrash(entity_opt) => {
let mut trash_items = Vec::new(); let mut trash_items = Vec::new();
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
if let Some(items) = tab.items_opt() { && let Some(items) = tab.items_opt()
{
for item in items { for item in items {
if item.selected { if item.selected {
if let ItemMetadata::Trash { entry, .. } = &item.metadata { if let ItemMetadata::Trash { entry, .. } = &item.metadata {
@ -3798,7 +3790,6 @@ impl Application for App {
} }
} }
} }
}
if !trash_items.is_empty() { if !trash_items.is_empty() {
return self.operation(Operation::Restore { items: trash_items }); return self.operation(Operation::Restore { items: trash_items });
} }
@ -3839,14 +3830,14 @@ impl Application for App {
// Close old context menu // Close old context menu
let active = self.tab_model.active(); let active = self.tab_model.active();
if let Some(tab) = self.tab_model.data_mut::<Tab>(active) { if let Some(tab) = self.tab_model.data_mut::<Tab>(active)
if tab.context_menu.is_some() { && tab.context_menu.is_some()
{
tasks.push(self.update(Message::TabMessage( tasks.push(self.update(Message::TabMessage(
Some(active), Some(active),
tab::Message::ContextMenu(None, None), tab::Message::ContextMenu(None, None),
))); )));
} }
}
// Activate new tab // Activate new tab
self.tab_model.activate(entity); self.tab_model.activate(entity);
@ -3937,7 +3928,6 @@ impl Application for App {
config.show_hidden = !config.show_hidden; config.show_hidden = !config.show_hidden;
return self.update(Message::TabConfig(config)); return self.update(Message::TabConfig(config));
} }
Message::TabMessage(entity_opt, tab_message) => { Message::TabMessage(entity_opt, tab_message) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
@ -4042,12 +4032,12 @@ impl Application for App {
// Destroy previous popup // Destroy previous popup
let mut window_ids = Vec::new(); let mut window_ids = Vec::new();
for (window_id, window) in &self.windows { for (window_id, window) in &self.windows {
if let WindowKind::ContextMenu(e, _) = &window.kind { if let WindowKind::ContextMenu(e, _) = &window.kind
if *e == entity { && *e == entity
{
window_ids.push(*window_id); window_ids.push(*window_id);
} }
} }
}
for window_id in window_ids { for window_id in window_ids {
commands.push(self.update(Message::Surface( commands.push(self.update(Message::Surface(
cosmic::surface::action::destroy_popup(window_id), cosmic::surface::action::destroy_popup(window_id),
@ -4402,12 +4392,14 @@ impl Application for App {
} }
} }
} }
Message::DndEnterTab(entity) => { Message::DndEnterTab(entity, mimes) => {
if mimes.iter().all(|m| m.as_str() != "x-cosmic-files/tab-dnd") {
self.tab_dnd_hover = Some((entity, Instant::now())); self.tab_dnd_hover = Some((entity, Instant::now()));
return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| { return Task::perform(tokio::time::sleep(HOVER_DURATION), move |()| {
cosmic::Action::App(Message::DndHoverTabTimeout(entity)) cosmic::Action::App(Message::DndHoverTabTimeout(entity))
}); });
} }
}
Message::DndExitTab => { Message::DndExitTab => {
self.nav_dnd_hover = None; self.nav_dnd_hover = None;
} }
@ -4451,14 +4443,14 @@ impl Application for App {
} }
} }
Message::NavBarClose(entity) => { Message::NavBarClose(entity) => {
if let Some(data) = self.nav_model.data::<MounterData>(entity) { if let Some(data) = self.nav_model.data::<MounterData>(entity)
if let Some(mounter) = MOUNTERS.get(&data.0) { && let Some(mounter) = MOUNTERS.get(&data.0)
{
return mounter return mounter
.unmount(data.1.clone()) .unmount(data.1.clone())
.map(|()| cosmic::action::none()); .map(|()| cosmic::action::none());
} }
} }
}
Message::NavBarContext(entity) => { Message::NavBarContext(entity) => {
self.nav_bar_context_id = entity; self.nav_bar_context_id = entity;
@ -4753,8 +4745,8 @@ impl Application for App {
if let Some(p) = paths.next() { if let Some(p) = paths.next() {
{ {
for (k, mounter_items) in &self.mounter_items { for (k, mounter_items) in &self.mounter_items {
if let Some(mounter) = MOUNTERS.get(k) { if let Some(mounter) = MOUNTERS.get(k)
if let Some(item) = mounter_items && let Some(item) = mounter_items
.iter() .iter()
.find(|&item| item.path().is_some_and(|path| path == p)) .find(|&item| item.path().is_some_and(|path| path == p))
{ {
@ -4767,7 +4759,6 @@ impl Application for App {
} }
} }
} }
}
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Message::Focused(id) => { Message::Focused(id) => {
if let Some(w) = self.windows.get(&id) { if let Some(w) = self.windows.get(&id) {
@ -4784,8 +4775,8 @@ impl Application for App {
} }
Message::SaveSortNames => { Message::SaveSortNames => {
self.must_save_sort_names = false; self.must_save_sort_names = false;
if let Some(state_handler) = self.state_handler.as_ref() { if let Some(state_handler) = self.state_handler.as_ref()
if let Err(err) = state_handler && let Err(err) = state_handler
.set::<&FxOrderMap<String, (HeadingOptions, bool)>>( .set::<&FxOrderMap<String, (HeadingOptions, bool)>>(
"sort_names", "sort_names",
&self.state.sort_names, &self.state.sort_names,
@ -4794,13 +4785,19 @@ impl Application for App {
log::warn!("Failed to save sort names: {err:?}"); log::warn!("Failed to save sort names: {err:?}");
} }
} }
}
Message::NetworkDriveOpenEntityAfterMount { entity } => { Message::NetworkDriveOpenEntityAfterMount { entity } => {
return self.on_nav_select(entity); return self.on_nav_select(entity);
} }
Message::NetworkDriveOpenTabAfterMount { location } => { Message::NetworkDriveOpenTabAfterMount { location } => {
return self.open_tab(location, false, None); return self.open_tab(location, false, None);
} }
Message::ReorderTab(ReorderEvent {
dragged,
target,
position,
}) => {
_ = self.tab_model.reorder(dragged, target, position);
}
} }
Task::none() Task::none()
@ -4881,14 +4878,14 @@ impl Application for App {
fn dialog(&self) -> Option<Element<'_, Message>> { fn dialog(&self) -> Option<Element<'_, Message>> {
//TODO: should gallery view just be a dialog? //TODO: should gallery view just be a dialog?
let entity = self.tab_model.active(); let entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data::<Tab>(entity) { if let Some(tab) = self.tab_model.data::<Tab>(entity)
if tab.gallery { && tab.gallery
{
return Some( return Some(
tab.gallery_view() tab.gallery_view()
.map(move |x| Message::TabMessage(Some(entity), x)), .map(move |x| Message::TabMessage(Some(entity), x)),
); );
} }
}
let dialog_page = self.dialog_pages.front()?; let dialog_page = self.dialog_pages.front()?;
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
@ -5770,8 +5767,9 @@ impl Application for App {
let mut tab_column = widget::column::with_capacity(4); let mut tab_column = widget::column::with_capacity(4);
if self.core.is_condensed() { if self.core.is_condensed()
if let Some(term) = self.search_get() { && let Some(term) = self.search_get()
{
tab_column = tab_column.push( tab_column = tab_column.push(
widget::container( widget::container(
widget::text_input::search_input("", term) widget::text_input::search_input("", term)
@ -5783,7 +5781,6 @@ impl Application for App {
.padding(space_xxs), .padding(space_xxs),
); );
} }
}
if self.tab_model.len() > 1 { if self.tab_model.len() > 1 {
tab_column = tab_column.push( tab_column = tab_column.push(
@ -5791,9 +5788,12 @@ impl Application for App {
widget::tab_bar::horizontal(&self.tab_model) widget::tab_bar::horizontal(&self.tab_model)
.button_height(32) .button_height(32)
.button_spacing(space_xxs) .button_spacing(space_xxs)
.enable_tab_drag(String::from("x-cosmic-files/tab-dnd"))
.on_reorder(move |event| Message::ReorderTab(event))
.tab_drag_threshold(25.)
.on_activate(Message::TabActivate) .on_activate(Message::TabActivate)
.on_close(|entity| Message::TabClose(Some(entity))) .on_close(|entity| Message::TabClose(Some(entity)))
.on_dnd_enter(|entity, _| Message::DndEnterTab(entity)) .on_dnd_enter(|entity, mimes| Message::DndEnterTab(entity, mimes))
.on_dnd_leave(|_| Message::DndExitTab) .on_dnd_leave(|_| Message::DndExitTab)
.on_dnd_drop(|entity, data, action| { .on_dnd_drop(|entity, data, action| {
Message::DndDropTab(entity, data, action) Message::DndDropTab(entity, data, action)
@ -6077,16 +6077,16 @@ impl Application for App {
let should_rescan = let should_rescan =
events.iter().any(|event| !event.kind.is_access()); events.iter().any(|event| !event.kind.is_access());
if should_rescan { if should_rescan
if let Err(e) = futures::executor::block_on(async { && let Err(e) = futures::executor::block_on(async {
output.send(Message::RescanTrash).await output.send(Message::RescanTrash).await
}) { })
{
log::warn!( log::warn!(
"trash needs to be rescanned but sending message failed: {e:?}" "trash needs to be rescanned but sending message failed: {e:?}"
); );
} }
} }
}
Err(e) => { Err(e) => {
log::warn!("failed to watch trash bin for changes: {e:?}"); log::warn!("failed to watch trash bin for changes: {e:?}");
} }
@ -6162,8 +6162,7 @@ impl Application for App {
|| kind.is_modify() || kind.is_modify()
|| kind.is_remove() || kind.is_remove()
|| kind.is_other() || kind.is_other()
}) { }) && let Err(e) = futures::executor::block_on(async {
if let Err(e) = futures::executor::block_on(async {
output.send(Message::RescanRecents).await output.send(Message::RescanRecents).await
}) { }) {
log::warn!( log::warn!(
@ -6171,7 +6170,6 @@ impl Application for App {
); );
} }
} }
}
Err(e) => { Err(e) => {
log::warn!("failed to watch recents file for changes: {e:?}") log::warn!("failed to watch recents file for changes: {e:?}")
} }
@ -6286,17 +6284,16 @@ impl Application for App {
let mut selected_previews = Vec::new(); let mut selected_previews = Vec::new();
match self.mode { match self.mode {
Mode::App => { Mode::App => {
if self.core.window.show_context { if self.core.window.show_context
if let ContextPage::Preview(entity_opt, PreviewKind::Selected) = && let ContextPage::Preview(entity_opt, PreviewKind::Selected) =
self.context_page self.context_page
{ {
selected_previews selected_previews
.push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active()))); .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active())));
} }
} }
}
Mode::Desktop => { Mode::Desktop => {
for window_kind in self.windows.iter().map(|(_, window)| &window.kind) { for window_kind in self.windows.values().map(|window| &window.kind) {
if let WindowKind::Preview(entity_opt, _) = window_kind { if let WindowKind::Preview(entity_opt, _) = window_kind {
selected_previews selected_previews
.push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active()))); .push(Some(entity_opt.unwrap_or_else(|| self.tab_model.active())));
@ -6311,8 +6308,7 @@ impl Application for App {
tab.subscription( tab.subscription(
selected_previews selected_previews
.iter() .iter()
.find(|preview| preview.as_ref() == Some(entity).as_ref()) .any(|preview| preview.as_ref() == Some(entity).as_ref()),
.is_some(),
) )
.with(entity) .with(entity)
.map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)), .map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)),

View file

@ -1,15 +1,15 @@
use std::{
collections::VecDeque,
fs,
io::{self, Read, Write},
path::Path,
};
use zip::result::ZipError;
use crate::{ use crate::{
mime_icon::mime_for_path, mime_icon::mime_for_path,
operation::{Controller, OpReader, OperationError, OperationErrorType}, operation::{Controller, OpReader, OperationError, OperationErrorType, sync_to_disk},
}; };
use cosmic::iced::futures;
use std::{
collections::HashSet,
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
};
use zip::result::ZipError;
pub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[ pub const SUPPORTED_ARCHIVE_TYPES: &[&str] = &[
"application/gzip", "application/gzip",
@ -113,27 +113,36 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
use std::{ffi::OsString, fs}; use std::{ffi::OsString, fs};
use zip::result::ZipError; use zip::result::ZipError;
fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> { fn make_writable_dir_all<T: AsRef<Path>>(
fs::create_dir_all(outpath.as_ref())?; outpath: T,
target_dirs: &mut HashSet<PathBuf>,
) -> Result<(), ZipError> {
let path = outpath.as_ref();
if !path.exists() {
fs::create_dir_all(path)?;
}
if !target_dirs.contains(path) {
target_dirs.insert(path.to_path_buf());
}
#[cfg(unix)] #[cfg(unix)]
{ {
// Dirs must be writable until all normal files are extracted // Dirs must be writable until all normal files are extracted
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions( fs::set_permissions(
outpath.as_ref(), path,
std::fs::Permissions::from_mode( fs::Permissions::from_mode(0o700 | fs::metadata(path)?.permissions().mode()),
0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
),
)?; )?;
} }
Ok(()) Ok(())
} }
#[cfg(unix)]
let mut files_by_unix_mode = Vec::new();
let mut buffer = vec![0; 4 * 1024 * 1024]; let mut buffer = vec![0; 4 * 1024 * 1024];
let total_files = archive.len(); let total_files = archive.len();
let mut pending_directory_creates = VecDeque::new(); let mut written_files = Vec::with_capacity(total_files);
let mut target_dirs = HashSet::new();
#[cfg(unix)]
let mut files_by_unix_mode = Vec::with_capacity(total_files);
for i in 0..total_files { for i in 0..total_files {
futures::executor::block_on(async { futures::executor::block_on(async {
@ -143,7 +152,7 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
.map_err(|s| io::Error::other(OperationError::from_state(s, &controller))) .map_err(|s| io::Error::other(OperationError::from_state(s, &controller)))
})?; })?;
controller.set_progress((i as f32) / total_files as f32); controller.set_progress(i as f32 / total_files as f32);
let mut file = match password { let mut file = match password {
None => archive.by_index(i), None => archive.by_index(i),
@ -156,26 +165,22 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
let outpath = directory.as_ref().join(filepath); let outpath = directory.as_ref().join(filepath);
if file.is_dir() { if file.is_dir() {
pending_directory_creates.push_back(outpath.clone()); make_writable_dir_all(&outpath, &mut target_dirs)?;
#[cfg(unix)]
if let Some(mode) = file.unix_mode() {
files_by_unix_mode.push((outpath, mode));
}
continue; continue;
} }
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
let mut target = Vec::with_capacity(file.size() as usize); if let Some(parent) = outpath.parent() {
file.read_to_end(&mut target)?; make_writable_dir_all(parent, &mut target_dirs)?;
Some(target)
} else {
None
};
drop(file);
if let Some(target) = symlink_target {
// create all pending dirs
while let Some(pending_dir) = pending_directory_creates.pop_front() {
make_writable_dir_all(pending_dir)?;
} }
if let Some(p) = outpath.parent() { if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
make_writable_dir_all(p)?; let mut target = Vec::with_capacity(file.size() as usize);
} file.read_to_end(&mut target)?;
#[cfg(unix)] #[cfg(unix)]
{ {
@ -205,21 +210,10 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
std::os::windows::fs::symlink_file(target_path, outpath.as_path())?; std::os::windows::fs::symlink_file(target_path, outpath.as_path())?;
} }
} }
written_files.push(outpath);
continue; continue;
} }
let mut file = match password {
None => archive.by_index(i),
Some(pwd) => archive.by_index_decrypt(i, pwd.as_bytes()),
}?;
// create all pending dirs
while let Some(pending_dir) = pending_directory_creates.pop_front() {
make_writable_dir_all(pending_dir)?;
}
if let Some(p) = outpath.parent() {
make_writable_dir_all(p)?;
}
let total = file.size(); let total = file.size();
let mut outfile = fs::File::create(&outpath)?; let mut outfile = fs::File::create(&outpath)?;
@ -245,13 +239,14 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
controller.set_progress(total_progress); controller.set_progress(total_progress);
} }
} }
#[cfg(unix)]
{
// Check for real permissions, which we'll set in a second pass // Check for real permissions, which we'll set in a second pass
#[cfg(unix)]
if let Some(mode) = file.unix_mode() { if let Some(mode) = file.unix_mode() {
files_by_unix_mode.push((outpath.clone(), mode)); files_by_unix_mode.push((outpath.clone(), mode));
} }
}
written_files.push(outpath);
} }
#[cfg(unix)] #[cfg(unix)]
{ {
@ -260,11 +255,15 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
if files_by_unix_mode.len() > 1 { if files_by_unix_mode.len() > 1 {
// Ensure we update children's permissions before making a parent unwritable // Ensure we update children's permissions before making a parent unwritable
files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.clone())); files_by_unix_mode.sort_by_key(|(path, _)| Reverse(path.components().count()));
} }
for (path, mode) in files_by_unix_mode { for (path, mode) in files_by_unix_mode {
fs::set_permissions(&path, fs::Permissions::from_mode(mode))?; fs::set_permissions(&path, fs::Permissions::from_mode(mode))?;
} }
} }
// Flush files to disk
futures::executor::block_on(async { sync_to_disk(written_files, target_dirs).await });
Ok(()) Ok(())
} }

View file

@ -179,12 +179,12 @@ impl<T: AsRef<str>> From<T> for DialogLabel {
}); });
} }
if let Some(span) = spans.last_mut() { if let Some(span) = spans.last_mut()
if underline == span.underline { && underline == span.underline
{
span.text.push(c); span.text.push(c);
continue; continue;
} }
}
spans.push(DialogLabelSpan { spans.push(DialogLabelSpan {
text: String::from(c), text: String::from(c),
@ -718,14 +718,14 @@ impl App {
children.push(preview); children.push(preview);
} }
if children.is_empty() { if children.is_empty()
if let Some(item) = &self.tab.parent_item_opt { && let Some(item) = &self.tab.parent_item_opt
{
children.push(item.preview_view(None, military_time)); children.push(item.preview_view(None, military_time));
} }
} }
} }
} }
}
widget::column::with_children(children).into() widget::column::with_children(children).into()
} }
@ -1279,13 +1279,13 @@ impl Application for App {
return self.update(message); return self.update(message);
} }
if let Some(data) = self.nav_model.data::<MounterData>(entity) { if let Some(data) = self.nav_model.data::<MounterData>(entity)
if let Some(mounter) = MOUNTERS.get(&data.0) { && let Some(mounter) = MOUNTERS.get(&data.0)
{
return mounter return mounter
.mount(data.1.clone()) .mount(data.1.clone())
.map(|()| cosmic::action::none()); .map(|()| cosmic::action::none());
} }
}
Task::none() Task::none()
} }
@ -1322,11 +1322,11 @@ impl Application for App {
// Close the dialog if the focused widget is the dialog's main text input instead of // Close the dialog if the focused widget is the dialog's main text input instead of
// unfocussing the widget. // unfocussing the widget.
if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish() { if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish()
if self.dialog_text_input == focused { && self.dialog_text_input == focused
{
return self.update(Message::Cancel); return self.update(Message::Cancel);
} }
}
self.update(Message::Cancel) self.update(Message::Cancel)
} }
@ -1419,23 +1419,23 @@ impl Application for App {
} }
// Check key binds from accept label // Check key binds from accept label
if let Some(key_bind) = &self.accept_label.key_bind_opt { if let Some(key_bind) = &self.accept_label.key_bind_opt
if key_bind.matches(modifiers, &key) { && key_bind.matches(modifiers, &key)
{
return self.update(if self.flags.kind.save() { return self.update(if self.flags.kind.save() {
Message::Save(false) Message::Save(false)
} else { } else {
Message::Open Message::Open
}); });
} }
}
// Uncaptured keys with only shift modifiers go to the search or location box // Uncaptured keys with only shift modifiers go to the search or location box
if !modifiers.logo() if !modifiers.logo()
&& !modifiers.control() && !modifiers.control()
&& !modifiers.alt() && !modifiers.alt()
&& matches!(key, Key::Character(_)) && matches!(key, Key::Character(_))
&& let Some(text) = text
{ {
if let Some(text) = text {
match self.flags.config.type_to_search { match self.flags.config.type_to_search {
TypeToSearch::Recursive => { TypeToSearch::Recursive => {
let mut term = self.search_get().unwrap_or_default().to_string(); let mut term = self.search_get().unwrap_or_default().to_string();
@ -1456,11 +1456,11 @@ impl Application for App {
} }
TypeToSearch::SelectByPrefix => { TypeToSearch::SelectByPrefix => {
// Reset buffer if timeout elapsed // Reset buffer if timeout elapsed
if let Some(last_key) = self.type_select_last_key { if let Some(last_key) = self.type_select_last_key
if last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT
{
self.type_select_prefix.clear(); self.type_select_prefix.clear();
} }
}
// Accumulate character and select // Accumulate character and select
self.type_select_prefix.push_str(&text.to_lowercase()); self.type_select_prefix.push_str(&text.to_lowercase());
@ -1477,7 +1477,6 @@ impl Application for App {
} }
} }
} }
}
Message::ModifiersChanged(modifiers) => { Message::ModifiersChanged(modifiers) => {
self.modifiers = modifiers; self.modifiers = modifiers;
} }
@ -1486,24 +1485,25 @@ impl Application for App {
let mut unmounted = Vec::new(); let mut unmounted = Vec::new();
if let Some(old_items) = self.mounter_items.get(&mounter_key) { if let Some(old_items) = self.mounter_items.get(&mounter_key) {
for old_item in old_items { for old_item in old_items {
if let Some(old_path) = old_item.path() { if let Some(old_path) = old_item.path()
if old_item.is_mounted() { && old_item.is_mounted()
{
let mut still_mounted = false; let mut still_mounted = false;
for item in &mounter_items { for item in &mounter_items {
if let Some(path) = item.path() { if let Some(path) = item.path()
if path == old_path && item.is_mounted() { && path == old_path
&& item.is_mounted()
{
still_mounted = true; still_mounted = true;
break; break;
} }
} }
}
if !still_mounted { if !still_mounted {
unmounted.push(Location::Path(old_path)); unmounted.push(Location::Path(old_path));
} }
} }
} }
} }
}
// Go back to home in any tabs that were unmounted // Go back to home in any tabs that were unmounted
let mut commands = Vec::new(); let mut commands = Vec::new();
@ -1601,8 +1601,9 @@ impl Application for App {
let mut paths = Vec::new(); let mut paths = Vec::new();
if let Some(items) = self.tab.items_opt() { if let Some(items) = self.tab.items_opt() {
for item in items { for item in items {
if item.selected { if item.selected
if let Some(path) = item.path_opt() { && let Some(path) = item.path_opt()
{
paths.push(path.clone()); paths.push(path.clone());
let _ = update_recently_used( let _ = update_recently_used(
path, path,
@ -1613,7 +1614,6 @@ impl Application for App {
} }
} }
} }
}
// Ensure selection is allowed // Ensure selection is allowed
//TODO: improve tab logic so this doesn't block the open button so often //TODO: improve tab logic so this doesn't block the open button so often
@ -1640,13 +1640,13 @@ impl Application for App {
} }
// If we are in directory mode, return the current directory // If we are in directory mode, return the current directory
if self.flags.kind.is_dir() { if self.flags.kind.is_dir()
if let Location::Path(tab_path) = &self.tab.location { && let Location::Path(tab_path) = &self.tab.location
{
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()])); self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
return window::close(self.flags.window_id); return window::close(self.flags.window_id);
} }
} }
}
Message::Preview => { Message::Preview => {
self.context_page = ContextPage::Preview(None, PreviewKind::Selected); self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
return self.with_dialog_config(|config| { return self.with_dialog_config(|config| {
@ -1654,15 +1654,15 @@ impl Application for App {
}); });
} }
Message::Save(replace) => { Message::Save(replace) => {
if let DialogKind::SaveFile { filename } = &self.flags.kind { if let DialogKind::SaveFile { filename } = &self.flags.kind
if !filename.is_empty() { && !filename.is_empty()
if let Some(tab_path) = self.tab.location.path_opt() { && let Some(tab_path) = self.tab.location.path_opt()
{
let path = tab_path.join(filename); let path = tab_path.join(filename);
if path.is_dir() { if path.is_dir() {
// cd to directory // cd to directory
let message = Message::TabMessage(tab::Message::Location( let message =
Location::Path(path), Message::TabMessage(tab::Message::Location(Location::Path(path)));
));
return self.update(message); return self.update(message);
} else if !replace && path.exists() { } else if !replace && path.exists() {
self.dialog_pages.push_back(DialogPage::Replace { self.dialog_pages.push_back(DialogPage::Replace {
@ -1674,8 +1674,6 @@ impl Application for App {
return window::close(self.flags.window_id); return window::close(self.flags.window_id);
} }
} }
}
}
Message::ScrollTab(scroll_speed) => { Message::ScrollTab(scroll_speed) => {
return self.update(Message::TabMessage(tab::Message::ScrollTab( return self.update(Message::TabMessage(tab::Message::ScrollTab(
f32::from(scroll_speed) / 10.0, f32::from(scroll_speed) / 10.0,
@ -1703,17 +1701,15 @@ impl Application for App {
let tab_commands = self.tab.update(tab_message, self.modifiers); let tab_commands = self.tab.update(tab_message, self.modifiers);
// Update filename box when anything is selected // Update filename box when anything is selected
if let DialogKind::SaveFile { filename } = &mut self.flags.kind { if let DialogKind::SaveFile { filename } = &mut self.flags.kind
if let Some(click_i) = click_i_opt { && let Some(click_i) = click_i_opt
if let Some(items) = self.tab.items_opt() { && let Some(items) = self.tab.items_opt()
if let Some(item) = items.get(click_i) { && let Some(item) = items.get(click_i)
if item.selected && !item.metadata.is_dir() { && item.selected
&& !item.metadata.is_dir()
{
filename.clone_from(&item.name); filename.clone_from(&item.name);
} }
}
}
}
}
let mut commands = Vec::new(); let mut commands = Vec::new();
for tab_command in tab_commands { for tab_command in tab_commands {
@ -1840,8 +1836,9 @@ impl Application for App {
Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => { Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => {
if location == self.tab.location { if location == self.tab.location {
// Filter // Filter
if let Some(filter_i) = self.filter_selected { if let Some(filter_i) = self.filter_selected
if let Some(filter) = self.filters.get(filter_i) { && let Some(filter) = self.filters.get(filter_i)
{
// Parse globs (Mime implements PartialEq with &str, so no need to parse) // Parse globs (Mime implements PartialEq with &str, so no need to parse)
let mut parsed_globs = Vec::new(); let mut parsed_globs = Vec::new();
let mut mimes = Vec::new(); let mut mimes = Vec::new();
@ -1868,7 +1865,6 @@ impl Application for App {
|| parsed_globs.iter().any(|glob| glob.matches(&item.name)) || parsed_globs.iter().any(|glob| glob.matches(&item.name))
}); });
} }
}
// Select based on filename // Select based on filename
if let DialogKind::SaveFile { filename } = &self.flags.kind { if let DialogKind::SaveFile { filename } = &self.flags.kind {
@ -1944,8 +1940,9 @@ impl Application for App {
let mut col = widget::column::with_capacity(2); let mut col = widget::column::with_capacity(2);
if self.core.is_condensed() { if self.core.is_condensed()
if let Some(term) = self.search_get() { && let Some(term) = self.search_get()
{
col = col.push( col = col.push(
widget::container( widget::container(
widget::text_input::search_input("", term) widget::text_input::search_input("", term)
@ -1957,7 +1954,6 @@ impl Application for App {
.padding(space_xxs), .padding(space_xxs),
); );
} }
}
col = col.push( col = col.push(
self.tab self.tab

View file

@ -392,8 +392,9 @@ impl LargeImageManager {
generation: u64, generation: u64,
) -> bool { ) -> bool {
// Check if this decode is still current (not superseded by a newer one) // Check if this decode is still current (not superseded by a newer one)
if let Some(&current_gen) = self.decode_generations.get(&path) { if let Some(&current_gen) = self.decode_generations.get(&path)
if generation != current_gen { && generation != current_gen
{
log::info!( log::info!(
"Discarding outdated decode for {} (generation {} != current {})", "Discarding outdated decode for {} (generation {} != current {})",
path.display(), path.display(),
@ -402,7 +403,6 @@ impl LargeImageManager {
); );
return false; return false;
} }
}
log::info!( log::info!(
"Storing decoded image for {} (generation {})", "Storing decoded image for {} (generation {})",
@ -556,7 +556,7 @@ impl LargeImageManager {
/// Check if sufficient memory is available, clearing cache if needed. /// Check if sufficient memory is available, clearing cache if needed.
/// Returns true if memory is available, false otherwise. /// Returns true if memory is available, false otherwise.
fn ensure_memory_available(&mut self, path: &PathBuf, width: u32, height: u32) -> bool { fn ensure_memory_available(&mut self, path: &Path, width: u32, height: u32) -> bool {
let (has_memory, error_opt) = check_memory_available(width, height); let (has_memory, error_opt) = check_memory_available(width, height);
if has_memory { if has_memory {
@ -565,7 +565,7 @@ impl LargeImageManager {
if self.cache_is_empty() { if self.cache_is_empty() {
if let Some(error_msg) = error_opt { if let Some(error_msg) = error_opt {
self.store_error(path.clone(), error_msg); self.store_error(path.to_path_buf(), error_msg);
log::warn!( log::warn!(
"Cannot load {}: insufficient memory and cache is empty", "Cannot load {}: insufficient memory and cache is empty",
path.display() path.display()
@ -588,7 +588,7 @@ impl LargeImageManager {
} }
if let Some(error_msg) = error_opt_after { if let Some(error_msg) = error_opt_after {
self.store_error(path.clone(), error_msg); self.store_error(path.to_path_buf(), error_msg);
log::warn!( log::warn!(
"Cannot load {}: insufficient memory even after cache clear", "Cannot load {}: insufficient memory even after cache clear",
path.display() path.display()

View file

@ -62,13 +62,13 @@ pub static LOCALE: LazyLock<Locale> = LazyLock::new(|| {
} }
// Try language-only fallback (e.g., "en" from "en-US") // Try language-only fallback (e.g., "en" from "en-US")
if let Some(lang) = cleaned_locale.split('-').next() { if let Some(lang) = cleaned_locale.split('-').next()
if let Ok(locale) = Locale::try_from_str(lang) { && let Ok(locale) = Locale::try_from_str(lang)
{
return locale; return locale;
} }
} }
} }
}
log::warn!("No valid locale found in environment, using fallback"); log::warn!("No valid locale found in environment, using fallback");
Locale::try_from_str("en-US").expect("Failed to parse fallback locale 'en-US'") Locale::try_from_str("en-US").expect("Failed to parse fallback locale 'en-US'")
}); });

View file

@ -310,7 +310,7 @@ impl MimeAppCache {
for (mime, filenames) in list.removed_associations.iter() { for (mime, filenames) in list.removed_associations.iter() {
for filename in filenames { for filename in filenames {
log::trace!("remove {mime}={filename}"); log::trace!("remove {mime}={filename}");
if let Some(apps) = self.cache.get_mut(&mime) { if let Some(apps) = self.cache.get_mut(mime) {
apps.retain(|x| !filename_eq(&x.path, filename)); apps.retain(|x| !filename_eq(&x.path, filename));
} }
} }
@ -319,7 +319,7 @@ impl MimeAppCache {
for (mime, filenames) in list.default_apps.iter() { for (mime, filenames) in list.default_apps.iter() {
for filename in filenames { for filename in filenames {
log::trace!("default {mime}={filename}"); log::trace!("default {mime}={filename}");
if let Some(apps) = self.cache.get_mut(&mime) { if let Some(apps) = self.cache.get_mut(mime) {
let mut found = false; let mut found = false;
for app in apps.iter_mut() { for app in apps.iter_mut() {
if filename_eq(&app.path, filename) { if filename_eq(&app.path, filename) {

View file

@ -23,13 +23,12 @@ fn resolve_uri(uri: &str) -> (String, gio::File) {
TARGET_URI_ATTRIBUTE, TARGET_URI_ATTRIBUTE,
gio::FileQueryInfoFlags::NONE, gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE, gio::Cancellable::NONE,
) { ) && let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE)
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) { {
let resolved_uri = String::from(resolved_uri); let resolved_uri = String::from(resolved_uri);
let file = gio::File::for_uri(&resolved_uri); let file = gio::File::for_uri(&resolved_uri);
return (resolved_uri, file); return (resolved_uri, file);
} }
}
(uri.to_string(), file) (uri.to_string(), file)
} }
@ -60,7 +59,7 @@ fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
gio::Cancellable::NONE, gio::Cancellable::NONE,
) )
.ok() .ok()
.and_then(|info| Some(info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))) .map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.unwrap_or(true); // Default to remote if query fails .unwrap_or(true); // Default to remote if query fails
MounterItem::Gvfs(Item { MounterItem::Gvfs(Item {
@ -457,9 +456,9 @@ impl Gvfs {
log::info!("mount {name}: result {res:?}"); log::info!("mount {name}: result {res:?}");
// Update the mounter_item with mount information after successful mount // Update the mounter_item with mount information after successful mount
let mut updated_item = mounter_item.clone(); let mut updated_item = mounter_item.clone();
if res.is_ok() { if res.is_ok()
if let MounterItem::Gvfs(ref mut item) = updated_item { && let MounterItem::Gvfs(ref mut item) = updated_item
if let Some(mount) = volume_for_callback.get_mount() { && let Some(mount) = volume_for_callback.get_mount() {
let root = MountExt::root(&mount); let root = MountExt::root(&mount);
item.path_opt = root.path(); item.path_opt = root.path();
item.is_mounted = true; item.is_mounted = true;
@ -469,14 +468,9 @@ impl Gvfs {
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE, gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE, gio::Cancellable::NONE,
) )
.ok() .ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.and_then(|info| {
Some(info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
})
.unwrap_or(true); .unwrap_or(true);
} }
}
}
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(()));

View file

@ -246,9 +246,10 @@ struct State {
impl State { impl State {
fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> { fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> {
if let Some(drag_source) = self.drag_initiated { if let Some(drag_source) = self.drag_initiated
if let Some(position) = cursor.position().or(self.last_virtual_position) { && let Some(position) = cursor.position().or(self.last_virtual_position)
if position.distance(drag_source) > 1.0 { && position.distance(drag_source) > 1.0
{
let min_x = drag_source.x.min(position.x); let min_x = drag_source.x.min(position.x);
let max_x = drag_source.x.max(position.x); let max_x = drag_source.x.max(position.x);
let min_y = drag_source.y.min(position.y); let min_y = drag_source.y.min(position.y);
@ -258,8 +259,6 @@ impl State {
Size::new(max_x - min_x, max_y - min_y), Size::new(max_x - min_x, max_y - min_y),
)); ));
} }
}
}
None None
} }
@ -527,13 +526,13 @@ fn update<Message: Clone>(
let offset = layout.virtual_offset(); let offset = layout.virtual_offset();
let layout_bounds = layout.bounds(); let layout_bounds = layout.bounds();
let viewport_changed = state.viewport.map_or(true, |v| v != *viewport); let viewport_changed = state.viewport != Some(*viewport);
if let Some(message) = widget.on_resize.as_ref() { if let Some(message) = widget.on_resize.as_ref()
if viewport_changed { && viewport_changed
{
shell.publish(message(*viewport)); shell.publish(message(*viewport));
} }
}
state.viewport = Some(*viewport); state.viewport = Some(*viewport);
@ -664,11 +663,12 @@ fn update<Message: Clone>(
} }
} }
if let Some(message) = widget.on_right_press.as_ref() { if let Some(message) = widget.on_right_press.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
) { )
{
let point_opt = if widget.on_right_press_window_position { let point_opt = if widget.on_right_press_window_position {
cursor.position_over(layout_bounds).map(|mut p| { cursor.position_over(layout_bounds).map(|mut p| {
p.x -= offset.x; p.x -= offset.x;
@ -685,93 +685,91 @@ fn update<Message: Clone>(
} }
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_right_release.as_ref() { if let Some(message) = widget.on_right_release.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_middle_press.as_ref() { if let Some(message) = widget.on_middle_press.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_middle_release.as_ref() { if let Some(message) = widget.on_middle_release.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_back_press.as_ref() { if let Some(message) = widget.on_back_press.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_back_release.as_ref() { if let Some(message) = widget.on_back_release.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Back))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_forward_press.as_ref() { if let Some(message) = widget.on_forward_press.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(message) = widget.on_forward_release.as_ref() { if let Some(message) = widget.on_forward_release.as_ref()
if matches!( && matches!(
event, event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward)) Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Forward))
) { )
{
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
return event::Status::Captured; return event::Status::Captured;
} }
}
if let Some(on_scroll) = widget.on_scroll.as_ref() { if let Some(on_scroll) = widget.on_scroll.as_ref()
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
if let Some(message) = on_scroll(*delta) { && let Some(message) = on_scroll(*delta)
{
shell.publish(message); shell.publish(message);
return event::Status::Captured; return event::Status::Captured;
} }
}
}
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) { if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
shell.publish(message(drag_rect.intersection(&layout_bounds).map( shell.publish(message(drag_rect.intersection(&layout_bounds).map(

View file

@ -5,7 +5,7 @@ use crate::{
spawn_detached::spawn_detached, spawn_detached::spawn_detached,
tab, tab,
}; };
use cosmic::iced::futures::{SinkExt, channel::mpsc::Sender}; use cosmic::iced::futures::{self, SinkExt, StreamExt, channel::mpsc::Sender, stream};
use std::{ use std::{
borrow::Cow, borrow::Cow,
fmt::Formatter, fmt::Formatter,
@ -196,6 +196,31 @@ async fn copy_or_move(
.map_err(wrap_compio_spawn_error)? .map_err(wrap_compio_spawn_error)?
} }
pub async fn sync_to_disk(
written_files: Vec<PathBuf>,
target_dirs: std::collections::HashSet<PathBuf>,
) {
// Sync files to disk
stream::iter(written_files.into_iter().map(|path| async move {
if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await {
let _ = file.sync_all().await;
}
}))
.buffer_unordered(32)
.collect::<Vec<_>>()
.await;
// Sync directories to disk
stream::iter(target_dirs.into_iter().map(|path| async move {
if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await {
let _ = dir.sync_all().await;
}
}))
.buffer_unordered(16)
.collect::<Vec<_>>()
.await;
}
fn copy_unique_path(from: &Path, to: &Path) -> PathBuf { fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
// List of compound extensions to check // List of compound extensions to check
const COMPOUND_EXTENSIONS: &[&str] = &[ const COMPOUND_EXTENSIONS: &[&str] = &[
@ -934,11 +959,11 @@ impl Operation {
let dir_name = get_directory_name(file_name); let dir_name = get_directory_name(file_name);
let mut new_dir = to.join(dir_name); let mut new_dir = to.join(dir_name);
if new_dir.exists() { if new_dir.exists()
if let Some(new_dir_parent) = new_dir.parent() { && let Some(new_dir_parent) = new_dir.parent()
{
new_dir = copy_unique_path(&new_dir, new_dir_parent); new_dir = copy_unique_path(&new_dir, new_dir_parent);
} }
}
op_sel.ignored.push(path.clone()); op_sel.ignored.push(path.clone());
op_sel.selected.push(new_dir.clone()); op_sel.selected.push(new_dir.clone());
@ -1185,7 +1210,7 @@ mod tests {
path::PathBuf, path::PathBuf,
}; };
use cosmic::iced::futures::{StreamExt, channel::mpsc}; use cosmic::iced::futures::{StreamExt, channel::mpsc, future};
use log::debug; use log::debug;
use test_log::test; use test_log::test;
use tokio::sync; use tokio::sync;
@ -1239,7 +1264,7 @@ mod tests {
} }
}; };
futures::future::join(handle_messages, handle_copy).await.1 future::join(handle_messages, handle_copy).await.1
} }
#[test(compio::test)] #[test(compio::test)]

View file

@ -27,7 +27,7 @@ impl OpReader {
impl io::Read for OpReader { impl io::Read for OpReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
futures::executor::block_on(async { cosmic::iced::futures::executor::block_on(async {
self.controller self.controller
.check() .check()
.await .await

View file

@ -7,7 +7,7 @@ use std::time::Instant;
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc}; use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::operation::OperationError; use crate::operation::{OperationError, sync_to_disk};
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path}; use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
@ -57,6 +57,8 @@ impl Context {
) -> Result<bool, OperationError> { ) -> Result<bool, OperationError> {
let mut ops = Vec::new(); let mut ops = Vec::new();
let mut cleanup_ops = Vec::new(); let mut cleanup_ops = Vec::new();
let mut written_files = Vec::new();
let mut target_dirs = std::collections::HashSet::new();
for (from_parent, to_parent) in from_to_pairs { for (from_parent, to_parent) in from_to_pairs {
self.controller self.controller
.check() .check()
@ -136,10 +138,13 @@ impl Context {
}), }),
is_cleanup: false, is_cleanup: false,
}; };
if matches!(method, Method::Move { .. }) { if matches!(method, Method::Move { .. })
if let Some(cleanup_op) = op.move_cleanup_op() { && let Some(cleanup_op) = op.move_cleanup_op()
{
cleanup_ops.push(cleanup_op); cleanup_ops.push(cleanup_op);
} }
if let Some(parent) = op.to.parent() {
target_dirs.insert(parent.to_path_buf());
} }
ops.push(op); ops.push(op);
} }
@ -177,10 +182,19 @@ impl Context {
&self.controller, &self.controller,
) )
})? { })? {
if matches!(
op.kind,
OpKind::Copy
| OpKind::Move {
cross_device_copy: true
}
) {
written_files.push(op.to.clone());
}
// The from path is ignored in the operation selection if it is a top level item // The from path is ignored in the operation selection if it is a top level item
if self.op_sel.ignored.contains(&op.from) { if self.op_sel.ignored.contains(&op.from) {
// So add the to path to the selection // So add the to path to the selection
self.op_sel.selected.push(op.to.clone()); self.op_sel.selected.push(op.to);
} }
} else { } else {
// Cancelled // Cancelled
@ -188,6 +202,9 @@ impl Context {
} }
} }
// Flush files to disk
sync_to_disk(written_files, target_dirs).await;
Ok(true) Ok(true)
} }
@ -305,7 +322,7 @@ impl Op {
} }
} }
let (from_file, metadata, mut to_file) = futures::try_join!( let (from_file, metadata, mut to_file) = cosmic::iced::futures::try_join!(
async { async {
compio::fs::OpenOptions::new() compio::fs::OpenOptions::new()
.read(true) .read(true)
@ -411,8 +428,6 @@ impl Op {
} }
} }
} }
to_file.sync_all().await?;
} }
OpKind::Move { cross_device_copy } => { OpKind::Move { cross_device_copy } => {
// Remove `to` if overwriting and it is an existing file // Remove `to` if overwriting and it is an existing file

View file

@ -1,5 +1,8 @@
use chrono::{Datelike, Timelike, Utc};
use cosmic::{ use cosmic::{
Apply, Element, cosmic_theme, font, Apply, Element, cosmic_theme,
desktop::fde::{DesktopEntry, get_languages_from_env},
font,
iced::{ iced::{
Alignment, Alignment,
Border, Border,
@ -37,8 +40,6 @@ use cosmic::{
menu::{action::MenuAction, key_bind::KeyBind}, menu::{action::MenuAction, key_bind::KeyBind},
}, },
}; };
use chrono::{Datelike, Timelike, Utc};
use i18n_embed::LanguageLoader; use i18n_embed::LanguageLoader;
use icu::{ use icu::{
datetime::{ datetime::{
@ -615,7 +616,8 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
} }
fn get_desktop_file_display_name(path: &Path) -> Option<String> { fn get_desktop_file_display_name(path: &Path) -> Option<String> {
let entry = match freedesktop_entry_parser::parse_entry(path) { let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
Ok(ok) => ok, Ok(ok) => ok,
Err(err) => { Err(err) => {
log::warn!("failed to parse {}: {}", path.display(), err); log::warn!("failed to parse {}: {}", path.display(), err);
@ -623,14 +625,11 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
} }
}; };
entry entry.name(&locales).map(|s| s.into_owned())
.section("Desktop Entry")
.attr("Name")
.map(str::to_string)
} }
fn get_desktop_file_icon(path: &Path) -> Option<String> { fn get_desktop_file_icon(path: &Path) -> Option<String> {
let entry = match freedesktop_entry_parser::parse_entry(path) { let entry = match DesktopEntry::from_path::<&str>(path, None) {
Ok(ok) => ok, Ok(ok) => ok,
Err(err) => { Err(err) => {
log::warn!("failed to parse {}: {}", path.display(), err); log::warn!("failed to parse {}: {}", path.display(), err);
@ -638,10 +637,7 @@ fn get_desktop_file_icon(path: &Path) -> Option<String> {
} }
}; };
entry entry.icon().map(str::to_string)
.section("Desktop Entry")
.attr("Icon")
.map(str::to_string)
} }
/// Creates an icon handle from a desktop file's Icon field value. /// Creates an icon handle from a desktop file's Icon field value.
@ -656,17 +652,17 @@ fn desktop_icon_handle(icon: &str, size: u16) -> widget::icon::Handle {
} }
pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) { pub fn parse_desktop_file(path: &Path) -> (Option<String>, Option<String>) {
let entry = match freedesktop_entry_parser::parse_entry(path) { let locales = get_languages_from_env();
let entry = match DesktopEntry::from_path(path, Some(&locales)) {
Ok(ok) => ok, Ok(ok) => ok,
Err(err) => { Err(err) => {
log::warn!("failed to parse {}: {}", path.display(), err); log::warn!("failed to parse {}: {}", path.display(), err);
return (None, None); return (None, None);
} }
}; };
let section = entry.section("Desktop Entry");
( (
section.attr("Name").map(str::to_string), entry.name(&locales).map(|s| s.into_owned()),
section.attr("Icon").map(str::to_string), entry.icon().map(str::to_string),
) )
} }
@ -934,8 +930,9 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
{ {
if let Ok(path_meta) = fs::metadata(tab_path) { if let Ok(path_meta) = fs::metadata(tab_path)
if fs_kind(&path_meta) == FsKind::Gvfs { && fs_kind(&path_meta) == FsKind::Gvfs
{
let file = gio::File::for_path(tab_path); let file = gio::File::for_path(tab_path);
// gio crate expects a comma delimited string // gio crate expects a comma delimited string
@ -974,7 +971,6 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
} }
} }
} }
}
if !remote_scannable { if !remote_scannable {
match fs::read_dir(tab_path) { match fs::read_dir(tab_path) {
@ -1410,13 +1406,13 @@ impl EditLocation {
self.selected = Some(selected); self.selected = Some(selected);
// Automatically resolve if there is only one completion // Automatically resolve if there is only one completion
if completions.len() == 1 { if completions.len() == 1
if let Some(resolved) = self.resolve() { && let Some(resolved) = self.resolve()
{
self.location = resolved; self.location = resolved;
self.selected = None; self.selected = None;
} }
} }
}
} else { } else {
self.selected = None; self.selected = None;
} }
@ -2033,11 +2029,11 @@ impl ItemThumbnail {
if let Some((item_thumbnail, temp_file)) = if let Some((item_thumbnail, temp_file)) =
Self::generate_thumbnail_external(path, &mime, thumbnail_size, thumbnail_dir) Self::generate_thumbnail_external(path, &mime, thumbnail_size, thumbnail_dir)
{ {
if let Ok(cache) = thumbnail_cacher { if let Ok(cache) = thumbnail_cacher
if let Err(err) = cache.update_with_temp_file(temp_file) { && let Err(err) = cache.update_with_temp_file(temp_file)
{
log::warn!("failed to update cache for {}: {}", path.display(), err); log::warn!("failed to update cache for {}: {}", path.display(), err);
} }
}
return item_thumbnail; return item_thumbnail;
} }
@ -2076,17 +2072,16 @@ impl ItemThumbnail {
// If we weren't able to create a thumbnail, but we should have // If we weren't able to create a thumbnail, but we should have
// been able to, create a fail marker so that it isn't tried the // been able to, create a fail marker so that it isn't tried the
// next time. // next time.
if let Ok(cacher) = thumbnail_cacher { if let Ok(cacher) = thumbnail_cacher
if tried_supported_file { && tried_supported_file
if let Err(err) = cacher.create_fail_marker() { && let Err(err) = cacher.create_fail_marker()
{
log::warn!( log::warn!(
"failed to create thumbnail fail marker for {}: {}", "failed to create thumbnail fail marker for {}: {}",
path.display(), path.display(),
err err
); );
} }
}
}
Self::NotImage Self::NotImage
} }
@ -2273,14 +2268,14 @@ impl Item {
widget::button::icon(widget::icon::from_name("go-next-symbolic")) widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(Message::ItemRight), .on_press(Message::ItemRight),
); );
if self.can_gallery() { if self.can_gallery()
if let Some(_path) = self.path_opt() { && let Some(_path) = self.path_opt()
{
row = row.push( row = row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
.on_press(Message::Gallery(true)), .on_press(Message::Gallery(true)),
); );
} }
}
row.into() row.into()
} }
@ -2445,19 +2440,21 @@ impl Item {
} }
} }
if let Some(path) = self.path_opt() { if let Some(path) = self.path_opt()
if let Ok(img) = image::image_dimensions(path) { && let Ok(img) = image::image_dimensions(path)
{
let (width, height) = img; let (width, height) = img;
details = details.push(widget::text::body(format!("{width}x{height}"))); details = details.push(widget::text::body(format!("{width}x{height}")));
} }
}
column = column.push(details); column = column.push(details);
if let Some(path) = self.path_opt() { if let Some(path) = self.path_opt() {
if self.selected {
column = column.push( column = column.push(
widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))), widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))),
); );
} }
}
if !settings.is_empty() { if !settings.is_empty() {
let mut section = widget::settings::section(); let mut section = widget::settings::section();
@ -2635,13 +2632,12 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
.map_err(|s| OperationError::from_state(s, &controller))?; .map_err(|s| OperationError::from_state(s, &controller))?;
//TODO: report more errors? //TODO: report more errors?
if let Ok(entry) = entry_res { if let Ok(entry) = entry_res
if let Ok(metadata) = entry.metadata() { && let Ok(metadata) = entry.metadata()
if metadata.is_file() { && metadata.is_file()
{
total += metadata.len(); total += metadata.len();
} }
}
}
// Yield in case this process takes a while. // Yield in case this process takes a while.
tokio::task::yield_now().await; tokio::task::yield_now().await;
@ -2766,12 +2762,12 @@ impl Tab {
let selected = self.selected_locations(); let selected = self.selected_locations();
for item in &mut items { for item in &mut items {
item.selected = false; item.selected = false;
if let Some(location) = &item.location_opt { if let Some(location) = &item.location_opt
if selected.contains(location) { && selected.contains(location)
{
item.selected = true; item.selected = true;
} }
} }
}
self.items_opt = Some(items); self.items_opt = Some(items);
} }
@ -2788,14 +2784,13 @@ impl Tab {
for item in items.iter_mut() { for item in items.iter_mut() {
item.cut = false; item.cut = false;
if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt) if let Some(location_path) = item.location_opt.as_ref().and_then(Location::path_opt)
&& locations.contains(location_path)
{ {
if locations.contains(location_path) {
item.cut = true; item.cut = true;
} }
} }
} }
} }
}
pub fn selected_locations(&self) -> Vec<Location> { pub fn selected_locations(&self) -> Vec<Location> {
if let Some(ref items) = self.items_opt { if let Some(ref items) = self.items_opt {
@ -2881,15 +2876,15 @@ impl Tab {
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for (i, item) in items.iter_mut().enumerate() { for (i, item) in items.iter_mut().enumerate() {
item.selected = false; item.selected = false;
if let Some(path) = item.path_opt() { if let Some(path) = item.path_opt()
if paths.contains(path) { && paths.contains(path)
{
item.selected = true; item.selected = true;
self.select_focus = Some(i); self.select_focus = Some(i);
} }
} }
} }
} }
}
fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool { fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool {
let mut start = (row, col); let mut start = (row, col);
@ -3085,11 +3080,11 @@ impl Tab {
return Vec::new(); return Vec::new();
}; };
if let Some((w, h)) = original_dims { if let Some((w, h)) = original_dims
if !should_use_tiling(*w, *h) { && !should_use_tiling(*w, *h)
{
return Vec::new(); return Vec::new();
} }
}
let Some(path) = item.path_opt() else { let Some(path) = item.path_opt() else {
return Vec::new(); return Vec::new();
@ -3389,8 +3384,7 @@ impl Tab {
self.date_time_formatter = date_time_formatter(self.config.military_time); self.date_time_formatter = date_time_formatter(self.config.military_time);
self.time_formatter = time_formatter(self.config.military_time); self.time_formatter = time_formatter(self.config.military_time);
} }
if show_hidden_changed { if show_hidden_changed && let Location::Search(path, term, ..) = &self.location {
if let Location::Search(path, term, ..) = &self.location {
cd = Some(Location::Search( cd = Some(Location::Search(
path.clone(), path.clone(),
term.clone(), term.clone(),
@ -3398,7 +3392,6 @@ impl Tab {
Instant::now(), Instant::now(),
)); ));
} }
}
// Unhighlight all items when config changes // Unhighlight all items when config changes
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
@ -3418,14 +3411,15 @@ impl Tab {
self.location_context_menu_index = None; self.location_context_menu_index = None;
//TODO: hack for clearing selecting when right clicking empty space //TODO: hack for clearing selecting when right clicking empty space
if self.context_menu.is_some() && self.last_right_click.take().is_none() { if self.context_menu.is_some()
if let Some(ref mut items) = self.items_opt { && self.last_right_click.take().is_none()
&& let Some(ref mut items) = self.items_opt
{
for item in items.iter_mut() { for item in items.iter_mut() {
item.selected = false; item.selected = false;
} }
} }
} }
}
Message::LocationContextMenuPoint(point_opt) => { Message::LocationContextMenuPoint(point_opt) => {
self.context_menu = None; self.context_menu = None;
self.location_context_menu_point = point_opt; self.location_context_menu_point = point_opt;
@ -3510,13 +3504,13 @@ impl Tab {
} }
} }
Message::EditLocationComplete(selected) => { Message::EditLocationComplete(selected) => {
if let Some(mut edit_location) = self.edit_location.take() { if let Some(mut edit_location) = self.edit_location.take()
if !matches!(edit_location.location, Location::Network(..)) { && !matches!(edit_location.location, Location::Network(..))
{
edit_location.selected = Some(selected); edit_location.selected = Some(selected);
cd = edit_location.resolve(); cd = edit_location.resolve();
} }
} }
}
Message::EditLocationEnable => { Message::EditLocationEnable => {
commands.push(Command::Iced( commands.push(Command::Iced(
widget::text_input::focus(self.edit_location_id.clone()).into(), widget::text_input::focus(self.edit_location_id.clone()).into(),
@ -3530,11 +3524,11 @@ impl Tab {
&& edit_location && edit_location
.completions .completions
.as_ref() .as_ref()
.map_or(false, |completions| !completions.is_empty()) .is_some_and(|completions| !completions.is_empty())
&& edit_location && edit_location
.location .location
.path_opt() .path_opt()
.map_or(false, |path| !path.exists()) .is_some_and(|path| !path.exists())
{ {
edit_location.selected = Some(0); edit_location.selected = Some(0);
} }
@ -3632,21 +3626,21 @@ impl Tab {
} }
} }
Message::GoNext => { Message::GoNext => {
if let Some(history_i) = self.history_i.checked_add(1) { if let Some(history_i) = self.history_i.checked_add(1)
if let Some(location) = self.history.get(history_i) { && let Some(location) = self.history.get(history_i)
{
cd = Some(location.clone()); cd = Some(location.clone());
history_i_opt = Some(history_i); history_i_opt = Some(history_i);
} }
} }
}
Message::GoPrevious => { Message::GoPrevious => {
if let Some(history_i) = self.history_i.checked_sub(1) { if let Some(history_i) = self.history_i.checked_sub(1)
if let Some(location) = self.history.get(history_i) { && let Some(location) = self.history.get(history_i)
{
cd = Some(location.clone()); cd = Some(location.clone());
history_i_opt = Some(history_i); history_i_opt = Some(history_i);
} }
} }
}
Message::ItemDown => { Message::ItemDown => {
if let Some(edit_location) = &mut self.edit_location { if let Some(edit_location) = &mut self.edit_location {
edit_location.select(true); edit_location.select(true);
@ -3823,12 +3817,12 @@ impl Tab {
Message::LocationUp => { Message::LocationUp => {
// Sets location to the path's parent // Sets location to the path's parent
// Does nothing if path is root or location is Trash // Does nothing if path is root or location is Trash
if let Location::Path(ref path) = self.location { if let Location::Path(ref path) = self.location
if let Some(parent) = path.parent() { && let Some(parent) = path.parent()
{
cd = Some(Location::Path(parent.to_owned())); cd = Some(Location::Path(parent.to_owned()));
} }
} }
}
Message::Open(path_opt) => { Message::Open(path_opt) => {
match path_opt { match path_opt {
Some(path) => { Some(path) => {
@ -3868,27 +3862,25 @@ impl Tab {
match mode { match mode {
Mode::App => { Mode::App => {
if is_only_one_selected { if is_only_one_selected {
return ResolveResult::Cd(location.clone()); ResolveResult::Cd(location.clone())
} else { } else {
return ResolveResult::OpenInTab(path_opt.cloned()); ResolveResult::OpenInTab(path_opt.cloned())
} }
} }
Mode::Desktop => { Mode::Desktop => match location {
return match location {
Location::Trash => ResolveResult::OpenTrash, Location::Trash => ResolveResult::OpenTrash,
_ => ResolveResult::Open(path_opt.cloned()), _ => ResolveResult::Open(path_opt.cloned()),
}; },
}
Mode::Dialog(_) => { Mode::Dialog(_) => {
if is_only_one_selected { if is_only_one_selected {
return ResolveResult::Cd(location.clone()); ResolveResult::Cd(location.clone())
} else { } else {
return ResolveResult::Skip; ResolveResult::Skip
} }
} }
} }
} else { } else {
return ResolveResult::Open(path_opt.cloned()); ResolveResult::Open(path_opt.cloned())
} }
} }
let mut open_files = Vec::new(); let mut open_files = Vec::new();
@ -3933,8 +3925,8 @@ impl Tab {
if mod_ctrl || mod_shift { if mod_ctrl || mod_shift {
self.update(Message::Click(click_i_opt), modifiers); self.update(Message::Click(click_i_opt), modifiers);
} }
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt
if !click_i_opt && !click_i_opt
.is_some_and(|click_i| items.get(click_i).is_some_and(|x| x.selected)) .is_some_and(|click_i| items.get(click_i).is_some_and(|x| x.selected))
{ {
// If item not selected, clear selection on other items // If item not selected, clear selection on other items
@ -3942,7 +3934,6 @@ impl Tab {
item.selected = Some(i) == click_i_opt; item.selected = Some(i) == click_i_opt;
} }
} }
}
//TODO: hack for clearing selecting when right clicking empty space //TODO: hack for clearing selecting when right clicking empty space
self.last_right_click = click_i_opt; self.last_right_click = click_i_opt;
} }
@ -3988,13 +3979,13 @@ impl Tab {
} }
Message::Resize(viewport) => { Message::Resize(viewport) => {
// Scroll to ensure focused item still in view // Scroll to ensure focused item still in view
if self.viewport_opt.map(|v| v.size()) != Some(viewport.size()) { if self.viewport_opt.map(|v| v.size()) != Some(viewport.size())
if let Some(offset) = self.select_focus_scroll() { && let Some(offset) = self.select_focus_scroll()
{
commands.push(Command::Iced( commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(), scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
)); ));
} }
}
self.viewport_opt = Some(viewport); self.viewport_opt = Some(viewport);
} }
@ -4097,14 +4088,13 @@ impl Tab {
} }
} }
Message::SelectLast => { Message::SelectLast => {
if let Some(ref items) = self.items_opt { if let Some(ref items) = self.items_opt
if let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max() && let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max()
&& self.select_position(last_pos.0, last_pos.1, mod_shift)
{ {
if self.select_position(last_pos.0, last_pos.1, mod_shift) {
if let Some(offset) = self.select_focus_scroll() { if let Some(offset) = self.select_focus_scroll() {
commands.push(Command::Iced( commands.push(Command::Iced(
scrollable::scroll_to(self.scrollable_id.clone(), offset) scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
.into(),
)); ));
} }
if let Some(id) = self.select_focus_id() { if let Some(id) = self.select_focus_id() {
@ -4112,8 +4102,6 @@ impl Tab {
} }
} }
} }
}
}
Message::SetOpenWith(mime, id) => { Message::SetOpenWith(mime, id) => {
commands.push(Command::SetOpenWith(mime, id)); commands.push(Command::SetOpenWith(mime, id));
} }
@ -4134,15 +4122,15 @@ impl Tab {
} }
} }
Message::TabComplete(path, completions) => { Message::TabComplete(path, completions) => {
if let Some(edit_location) = &mut self.edit_location { if let Some(edit_location) = &mut self.edit_location
if edit_location.location.path_opt() == Some(&path) { && edit_location.location.path_opt() == Some(&path)
{
edit_location.completions = Some(completions); edit_location.completions = Some(completions);
commands.push(Command::Iced( commands.push(Command::Iced(
widget::text_input::focus(self.edit_location_id.clone()).into(), widget::text_input::focus(self.edit_location_id.clone()).into(),
)); ));
} }
} }
}
Message::Thumbnail(path, thumbnail) => { Message::Thumbnail(path, thumbnail) => {
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
let location = Location::Path(path); let location = Location::Path(path);
@ -4274,11 +4262,11 @@ impl Tab {
} }
Message::DirectorySize(path, dir_size) => { Message::DirectorySize(path, dir_size) => {
let location = Location::Path(path); let location = Location::Path(path);
if let Some(ref mut item) = self.parent_item_opt { if let Some(ref mut item) = self.parent_item_opt
if item.location_opt.as_ref() == Some(&location) { && item.location_opt.as_ref() == Some(&location)
{
item.dir_size.clone_from(&dir_size); item.dir_size.clone_from(&dir_size);
} }
}
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() { for item in items.iter_mut() {
if item.location_opt.as_ref() == Some(&location) { if item.location_opt.as_ref() == Some(&location) {
@ -4315,14 +4303,13 @@ impl Tab {
} else { } else {
// Select parent if location is not directory // Select parent if location is not directory
let mut selected_paths = None; let mut selected_paths = None;
if let Some(path) = location.path_opt() { if let Some(path) = location.path_opt()
if !path.is_dir() { && !path.is_dir()
if let Some(parent) = path.parent() { && let Some(parent) = path.parent()
{
selected_paths = Some(vec![path.clone()]); selected_paths = Some(vec![path.clone()]);
location = location.with_path(parent.to_path_buf()); location = location.with_path(parent.to_path_buf());
} }
}
}
if location != self.location || selected_paths.is_some() { if location != self.location || selected_paths.is_some() {
if location.path_opt().is_none_or(|path| path.is_dir()) { if location.path_opt().is_none_or(|path| path.is_dir()) {
if selected_paths.is_none() { if selected_paths.is_none() {
@ -4529,9 +4516,10 @@ impl Tab {
//TODO: display error messages when image not found? //TODO: display error messages when image not found?
let mut name_opt = None; let mut name_opt = None;
let mut element_opt: Option<Element<Message>> = None; let mut element_opt: Option<Element<Message>> = None;
if let Some(index) = self.select_focus { if let Some(index) = self.select_focus
if let Some(items) = &self.items_opt { && let Some(items) = &self.items_opt
if let Some(item) = items.get(index) { && let Some(item) = items.get(index)
{
name_opt = Some(widget::text::heading(&item.display_name)); name_opt = Some(widget::text::heading(&item.display_name));
match item match item
.thumbnail_opt .thumbnail_opt
@ -4597,8 +4585,7 @@ impl Tab {
widget::image(image_handle).into() widget::image(image_handle).into()
}; };
element_opt = element_opt = Some(widget::container(content).center(Length::Fill).into());
Some(widget::container(content).center(Length::Fill).into());
} }
ItemThumbnail::Svg(handle) => { ItemThumbnail::Svg(handle) => {
element_opt = Some( element_opt = Some(
@ -4610,21 +4597,15 @@ impl Tab {
} }
ItemThumbnail::Text(text) => { ItemThumbnail::Text(text) => {
element_opt = Some( element_opt = Some(
widget::container( widget::container(widget::text_editor(text).padding(space_xxs).class(
widget::text_editor(text).padding(space_xxs).class( cosmic::theme::iced::TextEditor::Custom(Box::new(text_editor_class)),
cosmic::theme::iced::TextEditor::Custom(Box::new( ))
text_editor_class,
)),
),
)
.center(Length::Fill) .center(Length::Fill)
.into(), .into(),
); );
} }
} }
} }
}
}
let mut column = widget::column::with_capacity(2); let mut column = widget::column::with_capacity(2);
column = column.push(widget::Space::with_height(Length::Fixed(space_m.into()))); column = column.push(widget::Space::with_height(Length::Fixed(space_m.into())));
@ -4860,8 +4841,9 @@ impl Tab {
); );
let mut popover = let mut popover =
widget::popover(text_input).position(widget::popover::Position::Bottom); widget::popover(text_input).position(widget::popover::Position::Bottom);
if let Some(completions) = &edit_location.completions { if let Some(completions) = &edit_location.completions
if !completions.is_empty() { && !completions.is_empty()
{
let mut column = let mut column =
widget::column::with_capacity(completions.len()).padding(space_xxs); widget::column::with_capacity(completions.len()).padding(space_xxs);
for (i, (name, _path)) in completions.iter().enumerate() { for (i, (name, _path)) in completions.iter().enumerate() {
@ -4886,7 +4868,6 @@ impl Tab {
.max_width(size.width - 140.0), .max_width(size.width - 140.0),
); );
} }
}
row = row.push(popover); row = row.push(popover);
let mut column = widget::column::with_capacity(4).padding([0, space_s]); let mut column = widget::column::with_capacity(4).padding([0, space_s]);
column = column.push(row); column = column.push(row);
@ -5911,14 +5892,14 @@ impl Tab {
.wayland_on_right_press_window_position(); .wayland_on_right_press_window_position();
let mut popover = widget::popover(mouse_area); let mut popover = widget::popover(mouse_area);
if let Some(point) = self.context_menu { if let Some(point) = self.context_menu
if !cfg!(feature = "wayland") || !crate::is_wayland() { && (!cfg!(feature = "wayland") || !crate::is_wayland())
let context_menu = menu::context_menu(self, key_binds, &modifiers); {
let context_menu = menu::context_menu(self, key_binds, modifiers);
popover = popover popover = popover
.popup(context_menu) .popup(context_menu)
.position(widget::popover::Position::Point(point)); .position(widget::popover::Position::Point(point));
} }
}
let mut tab_column = widget::column::with_capacity(3); let mut tab_column = widget::column::with_capacity(3);
if let Some(location_view) = location_view_opt { if let Some(location_view) = location_view_opt {
@ -5937,8 +5918,9 @@ impl Tab {
} }
match &self.location { match &self.location {
Location::Trash => { Location::Trash => {
if let Some(items) = self.items_opt() { if let Some(items) = self.items_opt()
if !items.is_empty() { && !items.is_empty()
{
tab_column = tab_column.push( tab_column = tab_column.push(
widget::layer_container(widget::row::with_children([ widget::layer_container(widget::row::with_children([
widget::horizontal_space().into(), widget::horizontal_space().into(),
@ -5953,7 +5935,6 @@ impl Tab {
); );
} }
} }
}
Location::Network(uri, _display_name, _path) if uri == "network:///" => { Location::Network(uri, _display_name, _path) if uri == "network:///" => {
tab_column = tab_column.push( tab_column = tab_column.push(
widget::layer_container(widget::row::with_children([ widget::layer_container(widget::row::with_children([
@ -6248,11 +6229,11 @@ impl Tab {
let mut selected_items: Vec<&Item> = let mut selected_items: Vec<&Item> =
items.iter().filter(|item| item.selected).collect(); items.iter().filter(|item| item.selected).collect();
if selected_items.is_empty() { if selected_items.is_empty()
if let Some(p) = self.parent_item_opt.as_ref() { && let Some(p) = self.parent_item_opt.as_ref()
{
selected_items.push(p) selected_items.push(p)
} }
}
for item in selected_items { for item in selected_items {
// Item must have a path // Item must have a path
if let Some(path) = item.path_opt().cloned() { if let Some(path) = item.path_opt().cloned() {

View file

@ -66,11 +66,11 @@ impl ThumbnailCacher {
if let (Some(cache_base_dir), Ok(metadata)) = ( if let (Some(cache_base_dir), Ok(metadata)) = (
THUMBNAIL_CACHE_BASE_DIR.as_ref(), THUMBNAIL_CACHE_BASE_DIR.as_ref(),
std::fs::metadata(&self.file_path), std::fs::metadata(&self.file_path),
) { ) && metadata.is_file()
if metadata.is_file() && self.file_path.starts_with(cache_base_dir) { && self.file_path.starts_with(cache_base_dir)
{
return CachedThumbnail::Valid((self.file_path.clone(), None)); return CachedThumbnail::Valid((self.file_path.clone(), None));
} }
}
// Use cached thumbnail if it is valid. // Use cached thumbnail if it is valid.
if self.is_thumbnail_valid(&self.thumbnail_path) { if self.is_thumbnail_valid(&self.thumbnail_path) {

View file

@ -1,6 +1,7 @@
// 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
use cosmic::desktop::fde::GenericEntry;
use mime_guess::Mime; use mime_guess::Mime;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::{ use std::{
@ -115,7 +116,7 @@ impl ThumbnailerCache {
//TODO: handle directory specific behavior //TODO: handle directory specific behavior
for path in thumbnailer_paths { for path in thumbnailer_paths {
let entry = match freedesktop_entry_parser::parse_entry(&path) { let entry = match GenericEntry::from_path(&path) {
Ok(ok) => ok, Ok(ok) => ok,
Err(err) => { Err(err) => {
log::warn!("failed to parse {}: {}", path.display(), err); log::warn!("failed to parse {}: {}", path.display(), err);
@ -124,12 +125,18 @@ impl ThumbnailerCache {
}; };
//TODO: use TryExec? //TODO: use TryExec?
let section = entry.section("Thumbnailer Entry"); let Some(section) = entry.group("Thumbnailer Entry") else {
let Some(exec) = section.attr("Exec") else { log::warn!(
"missing Thumbnailer Entry section for thumbnailer {}",
path.display()
);
continue;
};
let Some(exec) = section.entry("Exec") else {
log::warn!("missing Exec attribute for thumbnailer {}", path.display()); log::warn!("missing Exec attribute for thumbnailer {}", path.display());
continue; continue;
}; };
let Some(mime_types) = section.attr("MimeType") else { let Some(mime_types) = section.entry("MimeType") else {
log::warn!( log::warn!(
"missing MimeType attribute for thumbnailer {}", "missing MimeType attribute for thumbnailer {}",
path.display() path.display()