Merge branch 'master' into master
This commit is contained in:
commit
eb1218a0db
26 changed files with 2278 additions and 1660 deletions
972
Cargo.lock
generated
972
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
46
Cargo.toml
46
Cargo.toml
|
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "cosmic-files"
|
||||
version = "1.0.0"
|
||||
version = "1.0.5"
|
||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-only"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.90"
|
||||
|
||||
[dependencies]
|
||||
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 }
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11"
|
||||
freedesktop_entry_parser = "1.3"
|
||||
futures = "0.3.31"
|
||||
gio = { version = "0.21", optional = true }
|
||||
glib = { version = "0.21", optional = true }
|
||||
glob = "0.3"
|
||||
|
|
@ -24,9 +22,9 @@ image = "0.25"
|
|||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mime_guess = "2"
|
||||
notify-debouncer-full = "0.6"
|
||||
notify-debouncer-full = "0.7"
|
||||
notify-rust = { version = "4", optional = true }
|
||||
open = "5.3.2"
|
||||
open = "5.3.3"
|
||||
paste = "1.0"
|
||||
regex = "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" }
|
||||
url = "2.5"
|
||||
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-mime = { git = "https://github.com/ebassi/xdg-mime-rs" }
|
||||
# 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"
|
||||
tar = "0.4.44"
|
||||
lzma-rust2 = { version = "0.15.4", optional = true }
|
||||
ordermap = { version = "1.0.0", features = ["serde"] }
|
||||
lzma-rust2 = { version = "0.15.7", optional = true }
|
||||
ordermap = { version = "1.1.0", features = ["serde"] }
|
||||
# Internationalization
|
||||
i18n-embed = { version = "0.16", features = [
|
||||
"fluent-system",
|
||||
|
|
@ -54,10 +52,10 @@ i18n-embed = { version = "0.16", features = [
|
|||
] }
|
||||
i18n-embed-fl = "0.10"
|
||||
rust-embed = "8"
|
||||
slotmap = "1.0.7"
|
||||
slotmap = "1.1.1"
|
||||
recently-used-xbel = { git = "https://github.com/pop-os/recently-used-xbel.git" }
|
||||
zip = "7"
|
||||
uzers = "0.12.1"
|
||||
uzers = "0.12.2"
|
||||
md-5 = "0.10.6"
|
||||
png = "0.18"
|
||||
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.
|
||||
[dependencies.compio]
|
||||
# Patched to fix mtime: https://github.com/compio-rs/compio/pull/625
|
||||
# version = "0.17.0"
|
||||
git = "https://github.com/jackpot51/compio.git"
|
||||
version = "0.18"
|
||||
default-features = false
|
||||
features = ["fs", "io", "macros", "polling", "runtime"]
|
||||
|
||||
[dependencies.io-uring]
|
||||
version = "0.7.11"
|
||||
default-features = false
|
||||
optional = true
|
||||
|
||||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
default-features = false
|
||||
#TODO: a11y feature crashes
|
||||
features = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"]
|
||||
features = [
|
||||
"about",
|
||||
"autosize",
|
||||
"desktop",
|
||||
"multi-window",
|
||||
"tokio",
|
||||
"winit",
|
||||
"surface-message",
|
||||
]
|
||||
|
||||
[[example]]
|
||||
name = "gio-list"
|
||||
|
|
@ -107,11 +106,10 @@ default = [
|
|||
"wayland",
|
||||
]
|
||||
dbus-config = ["libcosmic/dbus-config"]
|
||||
desktop = ["libcosmic/desktop", "dep:cosmic-mime-apps", "dep:xdg"]
|
||||
desktop = ["dep:cosmic-mime-apps", "dep:xdg"]
|
||||
desktop-applet = []
|
||||
gvfs = ["dep:gio", "dep:glib"]
|
||||
io-uring = ["compio/io-uring", "dep:io-uring"]
|
||||
io-uring-bindgen = ["io-uring?/bindgen"]
|
||||
io-uring = ["compio/io-uring"]
|
||||
jemalloc = ["dep:tikv-jemallocator"]
|
||||
notify = ["dep:notify-rust"]
|
||||
wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cosmic-files-applet"
|
||||
version = "0.1.0"
|
||||
version = "1.0.5"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
12
debian/changelog
vendored
12
debian/changelog
vendored
|
|
@ -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
|
||||
|
||||
* Stable release.
|
||||
|
|
|
|||
|
|
@ -389,9 +389,9 @@ type-to-search-enter-path = Zadává cestu ke složce nebo souboru
|
|||
compress = Komprimovat
|
||||
eject = Vysunout
|
||||
extract-here = Extrahovat
|
||||
change-wallpaper = Změnit pozadí...
|
||||
change-wallpaper = Změnit tapetu...
|
||||
desktop-appearance = Vzhled plochy...
|
||||
display-settings = Nastavení displeje...
|
||||
display-settings = Nastavení obrazovky...
|
||||
reload-folder = Znovu načíst složku
|
||||
sort-z-a = Z-A
|
||||
sort-newest-first = Nejnovější první
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 = Үлкенінен кішісіне
|
||||
|
|
@ -68,7 +68,7 @@ dark = 다크
|
|||
light = 라이트
|
||||
# Context menu
|
||||
new-file = 새 파일...
|
||||
new-folder = 새 폴더
|
||||
new-folder = 새 폴더...
|
||||
open-in-terminal = 터미널에서 열기
|
||||
move-to-trash = 휴지통으로 이동
|
||||
restore-from-trash = 휴지통에서 복구
|
||||
|
|
@ -84,7 +84,7 @@ sort-by-size = 크기 순으로 정렬
|
|||
file = 파일
|
||||
new-tab = 새 탭
|
||||
new-window = 새 창
|
||||
rename = 이름 바꾸기
|
||||
rename = 이름 바꾸기...
|
||||
close-tab = 탭 닫기
|
||||
quit = 종료
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ view = 보기
|
|||
grid-view = 그리드 보기
|
||||
list-view = 목록 보기
|
||||
menu-settings = 설정...
|
||||
menu-about = 코스믹 파일에 대하여...
|
||||
menu-about = COSMIC 파일 정보...
|
||||
connect = 연결
|
||||
read-execute = 읽기 및 실행
|
||||
item-modified = 마지막 수정 일자: { $modified }
|
||||
|
|
@ -171,7 +171,7 @@ details = 세부 정보
|
|||
mounted-drives = 마운트된 드라이브
|
||||
mount-error = 드라이브에 접근할 수 없음
|
||||
extract-here = 압축 해제
|
||||
removed-from-recents = { recents }에서 { $items } 항목 제거됨
|
||||
removed-from-recents = { recents } 에서 { $items }개의 항목을 제거했습니다
|
||||
add-to-sidebar = 사이드 바에 추가
|
||||
item-created = 생성 일자: { $created }
|
||||
type-to-search-recursive = 현재 폴더와 하위 폴더 탐색
|
||||
|
|
@ -202,7 +202,7 @@ delete-permanently = 완전히 삭제
|
|||
networks = 네트워크
|
||||
write-only = 쓰기 전용
|
||||
today = 오늘
|
||||
permanently-delete-warning = { $target }이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다.
|
||||
permanently-delete-warning = { $target } 이(가) 완전히 삭제됩니다. 이 행동은 되돌릴 수 없습니다.
|
||||
empty-trash-warning = 휴지통의 항목이 완전히 삭제됩니다
|
||||
empty-trash = 휴지통 비우기
|
||||
empty-trash-title = 휴지통을 비울까요?
|
||||
|
|
@ -210,3 +210,64 @@ type-to-search = 입력하여 검색
|
|||
notification-in-progress = 파일 작업이 진행 중입니다
|
||||
permanently-delete-question = 완전히 삭제할까요?
|
||||
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
0
i18n/ms/cosmic_files.ftl
Normal file
|
|
@ -3,8 +3,8 @@ empty-folder = Lege map
|
|||
empty-folder-hidden = Lege map (met verborgen bestanden)
|
||||
no-results = Geen resultaten gevonden
|
||||
filesystem = Bestandssysteem
|
||||
home = Home
|
||||
networks = Netwerk
|
||||
home = Persoonlijke map
|
||||
networks = Netwerken
|
||||
notification-in-progress = Bestanden worden nog bewerkt
|
||||
trash = Prullenbak
|
||||
recents = Recente bestanden
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ size = Розмір
|
|||
|
||||
## Empty Trash Dialog
|
||||
|
||||
empty-trash = Спорожнити Смітник
|
||||
empty-trash-warning = Ви впевнені, що хочете остаточно видалити всі елементи зі Смітника?
|
||||
empty-trash = Спорожнити смітник
|
||||
empty-trash-warning = Елементи зі смітника будуть остаточно видалені
|
||||
|
||||
## New File/Folder Dialog
|
||||
|
||||
|
|
@ -25,11 +25,11 @@ create-new-file = Створити новий файл
|
|||
create-new-folder = Створити нову теку
|
||||
file-name = Назва файлу
|
||||
folder-name = Назва теки
|
||||
file-already-exists = Файл з такою назвою вже існує.
|
||||
folder-already-exists = Тека з такою назвою вже існує.
|
||||
name-hidden = Назви, що починаються з ".", будуть приховані.
|
||||
name-invalid = Назва не може бути "{ $filename }".
|
||||
name-no-slashes = Назва не може містити скісні риски.
|
||||
file-already-exists = Файл з такою назвою вже існує
|
||||
folder-already-exists = Тека з такою назвою вже існує
|
||||
name-hidden = Назви, що починаються з ".", будуть приховані
|
||||
name-invalid = Назва не може бути "{ $filename }"
|
||||
name-no-slashes = Назва не може містити скісні риски
|
||||
|
||||
## Open/Save Dialog
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ rename-folder = Перейменувати теку
|
|||
## Replace Dialog
|
||||
|
||||
replace = Замінити
|
||||
replace-title = " { $filename }" вже існує в цьому місці.
|
||||
replace-title = " { $filename }" вже існує в цьому місці
|
||||
replace-warning = Бажаєте замінити його тим, що зберігаєте? Замінювання перезапише його вміст.
|
||||
replace-warning-operation = Бажаєте замінити його? Замінювання перезапише його вміст.
|
||||
original-file = Початковий файл
|
||||
|
|
@ -185,7 +185,7 @@ remove = Вилучити
|
|||
cancelled = Скасовані
|
||||
no-results = Нічого не знайдено
|
||||
networks = Мережі
|
||||
notification-in-progress = Виконуються операції з файлами.
|
||||
notification-in-progress = Триває обробка файлів
|
||||
today = Сьогодні
|
||||
desktop-view-options = Параметри вигляду стільниці...
|
||||
show-on-desktop = Показувати на стільниці
|
||||
|
|
@ -219,9 +219,9 @@ open-with-title = Як ви бажаєте відкрити "{ $name }"?
|
|||
browse-store = Переглянути { $store }
|
||||
other-apps = Інші застосунки
|
||||
related-apps = Пов'язані застосунки
|
||||
permanently-delete-question = Вилучити остаточно
|
||||
permanently-delete-question = Остаточно видалити?
|
||||
delete = Вилучити
|
||||
permanently-delete-warning = Ви впевнені, що хочете остаточно вилучити { $target }? Дію неможливо скасувати.
|
||||
permanently-delete-warning = { $target } буде остаточно видалено. Цю дію не можна скасувати.
|
||||
set-executable-and-launch = Зробити виконуваним і запустити
|
||||
set-executable-and-launch-description = Бажаєте зробити "{ $name }" виконуваним і запустити його?
|
||||
set-and-launch = Зробити і запустити
|
||||
|
|
@ -239,8 +239,8 @@ read-write = Перегляд і запис
|
|||
read-write-execute = Перегляд, запис і виконання
|
||||
favorite-path-error = Помилка при відкритті каталогу
|
||||
favorite-path-error-description =
|
||||
Неможливо відкрити "{ $path }".
|
||||
Можливо, його не існує або у вас немає прав на відкриття.
|
||||
Неможливо відкрити "{ $path }"
|
||||
"{ $path }" можливо, його не існує або у вас немає прав на відкриття
|
||||
|
||||
Вилучити з бічної панелі?
|
||||
keep = Залишити
|
||||
|
|
@ -302,7 +302,7 @@ extracted =
|
|||
} з "{ $from }" до "{ $to }"
|
||||
setting-executable-and-launching = Встановлення "{ $name }" виконуваним і запуск
|
||||
set-executable-and-launched = Встановлено "{ $name }" виконуваним і запущено
|
||||
selected-items = { $items } обраних елементів
|
||||
selected-items = Вибрані { $items } елементи
|
||||
setting-permissions = Встановлення дозволів { $mode } для "{ $name }"
|
||||
set-permissions = Встановлено дозволи { $mode } для "{ $name }"
|
||||
show-details = Показати деталі
|
||||
|
|
@ -355,3 +355,5 @@ removed-from-recents =
|
|||
[one] елемент
|
||||
*[other] елементи
|
||||
} з { recents }
|
||||
empty-trash-title = Спорожити смітник?
|
||||
type-to-search-select = Вибирає перший відповідний файл або папку
|
||||
|
|
|
|||
0
i18n/uz/cosmic_files.ftl
Normal file
0
i18n/uz/cosmic_files.ftl
Normal file
794
src/app.rs
794
src/app.rs
File diff suppressed because it is too large
Load diff
109
src/archive.rs
109
src/archive.rs
|
|
@ -1,15 +1,15 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
use zip::result::ZipError;
|
||||
|
||||
use crate::{
|
||||
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] = &[
|
||||
"application/gzip",
|
||||
|
|
@ -113,27 +113,36 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
|||
use std::{ffi::OsString, fs};
|
||||
use zip::result::ZipError;
|
||||
|
||||
fn make_writable_dir_all<T: AsRef<Path>>(outpath: T) -> Result<(), ZipError> {
|
||||
fs::create_dir_all(outpath.as_ref())?;
|
||||
fn make_writable_dir_all<T: AsRef<Path>>(
|
||||
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)]
|
||||
{
|
||||
// Dirs must be writable until all normal files are extracted
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(
|
||||
outpath.as_ref(),
|
||||
std::fs::Permissions::from_mode(
|
||||
0o700 | std::fs::metadata(outpath.as_ref())?.permissions().mode(),
|
||||
),
|
||||
fs::set_permissions(
|
||||
path,
|
||||
fs::Permissions::from_mode(0o700 | fs::metadata(path)?.permissions().mode()),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let mut files_by_unix_mode = Vec::new();
|
||||
let mut buffer = vec![0; 4 * 1024 * 1024];
|
||||
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 {
|
||||
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)))
|
||||
})?;
|
||||
|
||||
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 {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
let symlink_target = if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
|
||||
|
||||
if let Some(parent) = outpath.parent() {
|
||||
make_writable_dir_all(parent, &mut target_dirs)?;
|
||||
}
|
||||
|
||||
if file.is_symlink() && (cfg!(unix) || cfg!(windows)) {
|
||||
let mut target = Vec::with_capacity(file.size() as usize);
|
||||
file.read_to_end(&mut target)?;
|
||||
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() {
|
||||
make_writable_dir_all(p)?;
|
||||
}
|
||||
|
||||
#[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())?;
|
||||
}
|
||||
}
|
||||
|
||||
written_files.push(outpath);
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for real permissions, which we'll set in a second pass
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Check for real permissions, which we'll set in a second pass
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
files_by_unix_mode.push((outpath.clone(), mode));
|
||||
}
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
files_by_unix_mode.push((outpath.clone(), mode));
|
||||
}
|
||||
|
||||
written_files.push(outpath);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -260,11 +255,15 @@ fn zip_extract<R: io::Read + io::Seek, P: AsRef<Path>>(
|
|||
|
||||
if files_by_unix_mode.len() > 1 {
|
||||
// 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 {
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
296
src/dialog.rs
296
src/dialog.rs
|
|
@ -179,11 +179,11 @@ impl<T: AsRef<str>> From<T> for DialogLabel {
|
|||
});
|
||||
}
|
||||
|
||||
if let Some(span) = spans.last_mut() {
|
||||
if underline == span.underline {
|
||||
span.text.push(c);
|
||||
continue;
|
||||
}
|
||||
if let Some(span) = spans.last_mut()
|
||||
&& underline == span.underline
|
||||
{
|
||||
span.text.push(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
spans.push(DialogLabelSpan {
|
||||
|
|
@ -718,10 +718,10 @@ impl App {
|
|||
children.push(preview);
|
||||
}
|
||||
|
||||
if children.is_empty() {
|
||||
if let Some(item) = &self.tab.parent_item_opt {
|
||||
children.push(item.preview_view(None, military_time));
|
||||
}
|
||||
if children.is_empty()
|
||||
&& let Some(item) = &self.tab.parent_item_opt
|
||||
{
|
||||
children.push(item.preview_view(None, military_time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1279,12 +1279,12 @@ impl Application for App {
|
|||
return self.update(message);
|
||||
}
|
||||
|
||||
if let Some(data) = self.nav_model.data::<MounterData>(entity) {
|
||||
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
||||
return mounter
|
||||
.mount(data.1.clone())
|
||||
.map(|()| cosmic::action::none());
|
||||
}
|
||||
if let Some(data) = self.nav_model.data::<MounterData>(entity)
|
||||
&& let Some(mounter) = MOUNTERS.get(&data.0)
|
||||
{
|
||||
return mounter
|
||||
.mount(data.1.clone())
|
||||
.map(|()| cosmic::action::none());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
|
@ -1322,10 +1322,10 @@ impl Application for App {
|
|||
|
||||
// Close the dialog if the focused widget is the dialog's main text input instead of
|
||||
// unfocussing the widget.
|
||||
if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish() {
|
||||
if self.dialog_text_input == focused {
|
||||
return self.update(Message::Cancel);
|
||||
}
|
||||
if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish()
|
||||
&& self.dialog_text_input == focused
|
||||
{
|
||||
return self.update(Message::Cancel);
|
||||
}
|
||||
|
||||
self.update(Message::Cancel)
|
||||
|
|
@ -1419,14 +1419,14 @@ impl Application for App {
|
|||
}
|
||||
|
||||
// Check key binds from accept label
|
||||
if let Some(key_bind) = &self.accept_label.key_bind_opt {
|
||||
if key_bind.matches(modifiers, &key) {
|
||||
return self.update(if self.flags.kind.save() {
|
||||
Message::Save(false)
|
||||
} else {
|
||||
Message::Open
|
||||
});
|
||||
}
|
||||
if let Some(key_bind) = &self.accept_label.key_bind_opt
|
||||
&& key_bind.matches(modifiers, &key)
|
||||
{
|
||||
return self.update(if self.flags.kind.save() {
|
||||
Message::Save(false)
|
||||
} else {
|
||||
Message::Open
|
||||
});
|
||||
}
|
||||
|
||||
// Uncaptured keys with only shift modifiers go to the search or location box
|
||||
|
|
@ -1434,45 +1434,44 @@ impl Application for App {
|
|||
&& !modifiers.control()
|
||||
&& !modifiers.alt()
|
||||
&& matches!(key, Key::Character(_))
|
||||
&& let Some(text) = text
|
||||
{
|
||||
if let Some(text) = text {
|
||||
match self.flags.config.type_to_search {
|
||||
TypeToSearch::Recursive => {
|
||||
let mut term = self.search_get().unwrap_or_default().to_string();
|
||||
term.push_str(&text);
|
||||
return self.search_set(Some(term));
|
||||
match self.flags.config.type_to_search {
|
||||
TypeToSearch::Recursive => {
|
||||
let mut term = self.search_get().unwrap_or_default().to_string();
|
||||
term.push_str(&text);
|
||||
return self.search_set(Some(term));
|
||||
}
|
||||
TypeToSearch::EnterPath => {
|
||||
let location = (self.tab.edit_location)
|
||||
.as_ref()
|
||||
.map_or_else(|| &self.tab.location, |x| &x.location);
|
||||
// Try to add text to end of location
|
||||
if let Some(path) = location.path_opt() {
|
||||
let mut path_string = path.to_string_lossy().to_string();
|
||||
path_string.push_str(&text);
|
||||
self.tab.edit_location =
|
||||
Some(location.with_path(PathBuf::from(path_string)).into());
|
||||
}
|
||||
TypeToSearch::EnterPath => {
|
||||
let location = (self.tab.edit_location)
|
||||
.as_ref()
|
||||
.map_or_else(|| &self.tab.location, |x| &x.location);
|
||||
// Try to add text to end of location
|
||||
if let Some(path) = location.path_opt() {
|
||||
let mut path_string = path.to_string_lossy().to_string();
|
||||
path_string.push_str(&text);
|
||||
self.tab.edit_location =
|
||||
Some(location.with_path(PathBuf::from(path_string)).into());
|
||||
}
|
||||
}
|
||||
TypeToSearch::SelectByPrefix => {
|
||||
// Reset buffer if timeout elapsed
|
||||
if let Some(last_key) = self.type_select_last_key
|
||||
&& last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT
|
||||
{
|
||||
self.type_select_prefix.clear();
|
||||
}
|
||||
TypeToSearch::SelectByPrefix => {
|
||||
// Reset buffer if timeout elapsed
|
||||
if let Some(last_key) = self.type_select_last_key {
|
||||
if last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT {
|
||||
self.type_select_prefix.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate character and select
|
||||
self.type_select_prefix.push_str(&text.to_lowercase());
|
||||
self.type_select_last_key = Some(Instant::now());
|
||||
// Accumulate character and select
|
||||
self.type_select_prefix.push_str(&text.to_lowercase());
|
||||
self.type_select_last_key = Some(Instant::now());
|
||||
|
||||
self.tab.select_by_prefix(&self.type_select_prefix);
|
||||
if let Some(offset) = self.tab.select_focus_scroll() {
|
||||
return scrollable::scroll_to(
|
||||
self.tab.scrollable_id.clone(),
|
||||
offset,
|
||||
);
|
||||
}
|
||||
self.tab.select_by_prefix(&self.type_select_prefix);
|
||||
if let Some(offset) = self.tab.select_focus_scroll() {
|
||||
return scrollable::scroll_to(
|
||||
self.tab.scrollable_id.clone(),
|
||||
offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1486,21 +1485,22 @@ impl Application for App {
|
|||
let mut unmounted = Vec::new();
|
||||
if let Some(old_items) = self.mounter_items.get(&mounter_key) {
|
||||
for old_item in old_items {
|
||||
if let Some(old_path) = old_item.path() {
|
||||
if old_item.is_mounted() {
|
||||
let mut still_mounted = false;
|
||||
for item in &mounter_items {
|
||||
if let Some(path) = item.path() {
|
||||
if path == old_path && item.is_mounted() {
|
||||
still_mounted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !still_mounted {
|
||||
unmounted.push(Location::Path(old_path));
|
||||
if let Some(old_path) = old_item.path()
|
||||
&& old_item.is_mounted()
|
||||
{
|
||||
let mut still_mounted = false;
|
||||
for item in &mounter_items {
|
||||
if let Some(path) = item.path()
|
||||
&& path == old_path
|
||||
&& item.is_mounted()
|
||||
{
|
||||
still_mounted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !still_mounted {
|
||||
unmounted.push(Location::Path(old_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1601,16 +1601,16 @@ impl Application for App {
|
|||
let mut paths = Vec::new();
|
||||
if let Some(items) = self.tab.items_opt() {
|
||||
for item in items {
|
||||
if item.selected {
|
||||
if let Some(path) = item.path_opt() {
|
||||
paths.push(path.clone());
|
||||
let _ = update_recently_used(
|
||||
path,
|
||||
Self::APP_ID.to_string(),
|
||||
"cosmic-files".to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
if item.selected
|
||||
&& let Some(path) = item.path_opt()
|
||||
{
|
||||
paths.push(path.clone());
|
||||
let _ = update_recently_used(
|
||||
path,
|
||||
Self::APP_ID.to_string(),
|
||||
"cosmic-files".to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1640,11 +1640,11 @@ impl Application for App {
|
|||
}
|
||||
|
||||
// If we are in directory mode, return the current directory
|
||||
if self.flags.kind.is_dir() {
|
||||
if let Location::Path(tab_path) = &self.tab.location {
|
||||
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
|
||||
return window::close(self.flags.window_id);
|
||||
}
|
||||
if self.flags.kind.is_dir()
|
||||
&& let Location::Path(tab_path) = &self.tab.location
|
||||
{
|
||||
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
|
||||
return window::close(self.flags.window_id);
|
||||
}
|
||||
}
|
||||
Message::Preview => {
|
||||
|
|
@ -1654,26 +1654,24 @@ impl Application for App {
|
|||
});
|
||||
}
|
||||
Message::Save(replace) => {
|
||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||
if !filename.is_empty() {
|
||||
if let Some(tab_path) = self.tab.location.path_opt() {
|
||||
let path = tab_path.join(filename);
|
||||
if path.is_dir() {
|
||||
// cd to directory
|
||||
let message = Message::TabMessage(tab::Message::Location(
|
||||
Location::Path(path),
|
||||
));
|
||||
return self.update(message);
|
||||
} else if !replace && path.exists() {
|
||||
self.dialog_pages.push_back(DialogPage::Replace {
|
||||
filename: filename.clone(),
|
||||
});
|
||||
return widget::button::focus(REPLACE_BUTTON_ID.clone());
|
||||
}
|
||||
self.result_opt = Some(DialogResult::Open(vec![path]));
|
||||
return window::close(self.flags.window_id);
|
||||
}
|
||||
if let DialogKind::SaveFile { filename } = &self.flags.kind
|
||||
&& !filename.is_empty()
|
||||
&& let Some(tab_path) = self.tab.location.path_opt()
|
||||
{
|
||||
let path = tab_path.join(filename);
|
||||
if path.is_dir() {
|
||||
// cd to directory
|
||||
let message =
|
||||
Message::TabMessage(tab::Message::Location(Location::Path(path)));
|
||||
return self.update(message);
|
||||
} else if !replace && path.exists() {
|
||||
self.dialog_pages.push_back(DialogPage::Replace {
|
||||
filename: filename.clone(),
|
||||
});
|
||||
return widget::button::focus(REPLACE_BUTTON_ID.clone());
|
||||
}
|
||||
self.result_opt = Some(DialogResult::Open(vec![path]));
|
||||
return window::close(self.flags.window_id);
|
||||
}
|
||||
}
|
||||
Message::ScrollTab(scroll_speed) => {
|
||||
|
|
@ -1703,16 +1701,14 @@ impl Application for App {
|
|||
let tab_commands = self.tab.update(tab_message, self.modifiers);
|
||||
|
||||
// Update filename box when anything is selected
|
||||
if let DialogKind::SaveFile { filename } = &mut self.flags.kind {
|
||||
if let Some(click_i) = click_i_opt {
|
||||
if let Some(items) = self.tab.items_opt() {
|
||||
if let Some(item) = items.get(click_i) {
|
||||
if item.selected && !item.metadata.is_dir() {
|
||||
filename.clone_from(&item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let DialogKind::SaveFile { filename } = &mut self.flags.kind
|
||||
&& let Some(click_i) = click_i_opt
|
||||
&& let Some(items) = self.tab.items_opt()
|
||||
&& let Some(item) = items.get(click_i)
|
||||
&& item.selected
|
||||
&& !item.metadata.is_dir()
|
||||
{
|
||||
filename.clone_from(&item.name);
|
||||
}
|
||||
|
||||
let mut commands = Vec::new();
|
||||
|
|
@ -1840,34 +1836,34 @@ impl Application for App {
|
|||
Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => {
|
||||
if location == self.tab.location {
|
||||
// Filter
|
||||
if let Some(filter_i) = self.filter_selected {
|
||||
if let Some(filter) = self.filters.get(filter_i) {
|
||||
// Parse globs (Mime implements PartialEq with &str, so no need to parse)
|
||||
let mut parsed_globs = Vec::new();
|
||||
let mut mimes = Vec::new();
|
||||
for pattern in &filter.patterns {
|
||||
match pattern {
|
||||
DialogFilterPattern::Glob(value) => {
|
||||
match glob::Pattern::new(value) {
|
||||
Ok(glob) => parsed_globs.push(glob),
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse glob {value:?}: {err}");
|
||||
}
|
||||
if let Some(filter_i) = self.filter_selected
|
||||
&& let Some(filter) = self.filters.get(filter_i)
|
||||
{
|
||||
// Parse globs (Mime implements PartialEq with &str, so no need to parse)
|
||||
let mut parsed_globs = Vec::new();
|
||||
let mut mimes = Vec::new();
|
||||
for pattern in &filter.patterns {
|
||||
match pattern {
|
||||
DialogFilterPattern::Glob(value) => {
|
||||
match glob::Pattern::new(value) {
|
||||
Ok(glob) => parsed_globs.push(glob),
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse glob {value:?}: {err}");
|
||||
}
|
||||
}
|
||||
DialogFilterPattern::Mime(value) => mimes.push(value.as_str()),
|
||||
}
|
||||
DialogFilterPattern::Mime(value) => mimes.push(value.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
items.retain(|item| {
|
||||
// Directories are always shown
|
||||
item.metadata.is_dir()
|
||||
items.retain(|item| {
|
||||
// Directories are always shown
|
||||
item.metadata.is_dir()
|
||||
// Check for mime type match (first because it is faster)
|
||||
|| mimes.iter().copied().any(|mime| mime == item.mime)
|
||||
// Check for glob match (last because it is slower)
|
||||
|| parsed_globs.iter().any(|glob| glob.matches(&item.name))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Select based on filename
|
||||
|
|
@ -1944,19 +1940,19 @@ impl Application for App {
|
|||
|
||||
let mut col = widget::column::with_capacity(2);
|
||||
|
||||
if self.core.is_condensed() {
|
||||
if let Some(term) = self.search_get() {
|
||||
col = col.push(
|
||||
widget::container(
|
||||
widget::text_input::search_input("", term)
|
||||
.width(Length::Fill)
|
||||
.id(self.search_id.clone())
|
||||
.on_clear(Message::SearchClear)
|
||||
.on_input(Message::SearchInput),
|
||||
)
|
||||
.padding(space_xxs),
|
||||
);
|
||||
}
|
||||
if self.core.is_condensed()
|
||||
&& let Some(term) = self.search_get()
|
||||
{
|
||||
col = col.push(
|
||||
widget::container(
|
||||
widget::text_input::search_input("", term)
|
||||
.width(Length::Fill)
|
||||
.id(self.search_id.clone())
|
||||
.on_clear(Message::SearchClear)
|
||||
.on_input(Message::SearchInput),
|
||||
)
|
||||
.padding(space_xxs),
|
||||
);
|
||||
}
|
||||
|
||||
col = col.push(
|
||||
|
|
|
|||
|
|
@ -392,16 +392,16 @@ impl LargeImageManager {
|
|||
generation: u64,
|
||||
) -> bool {
|
||||
// Check if this decode is still current (not superseded by a newer one)
|
||||
if let Some(¤t_gen) = self.decode_generations.get(&path) {
|
||||
if generation != current_gen {
|
||||
log::info!(
|
||||
"Discarding outdated decode for {} (generation {} != current {})",
|
||||
path.display(),
|
||||
generation,
|
||||
current_gen
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if let Some(¤t_gen) = self.decode_generations.get(&path)
|
||||
&& generation != current_gen
|
||||
{
|
||||
log::info!(
|
||||
"Discarding outdated decode for {} (generation {} != current {})",
|
||||
path.display(),
|
||||
generation,
|
||||
current_gen
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
|
|
@ -556,7 +556,7 @@ impl LargeImageManager {
|
|||
|
||||
/// Check if sufficient memory is available, clearing cache if needed.
|
||||
/// 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);
|
||||
|
||||
if has_memory {
|
||||
|
|
@ -565,7 +565,7 @@ impl LargeImageManager {
|
|||
|
||||
if self.cache_is_empty() {
|
||||
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!(
|
||||
"Cannot load {}: insufficient memory and cache is empty",
|
||||
path.display()
|
||||
|
|
@ -588,7 +588,7 @@ impl LargeImageManager {
|
|||
}
|
||||
|
||||
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!(
|
||||
"Cannot load {}: insufficient memory even after cache clear",
|
||||
path.display()
|
||||
|
|
|
|||
|
|
@ -62,10 +62,10 @@ pub static LOCALE: LazyLock<Locale> = LazyLock::new(|| {
|
|||
}
|
||||
|
||||
// Try language-only fallback (e.g., "en" from "en-US")
|
||||
if let Some(lang) = cleaned_locale.split('-').next() {
|
||||
if let Ok(locale) = Locale::try_from_str(lang) {
|
||||
return locale;
|
||||
}
|
||||
if let Some(lang) = cleaned_locale.split('-').next()
|
||||
&& let Ok(locale) = Locale::try_from_str(lang)
|
||||
{
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ impl MimeAppCache {
|
|||
for (mime, filenames) in list.removed_associations.iter() {
|
||||
for filename in filenames {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -319,7 +319,7 @@ impl MimeAppCache {
|
|||
for (mime, filenames) in list.default_apps.iter() {
|
||||
for filename in filenames {
|
||||
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;
|
||||
for app in apps.iter_mut() {
|
||||
if filename_eq(&app.path, filename) {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,11 @@ fn resolve_uri(uri: &str) -> (String, gio::File) {
|
|||
TARGET_URI_ATTRIBUTE,
|
||||
gio::FileQueryInfoFlags::NONE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
if let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE) {
|
||||
let resolved_uri = String::from(resolved_uri);
|
||||
let file = gio::File::for_uri(&resolved_uri);
|
||||
return (resolved_uri, file);
|
||||
}
|
||||
) && let Some(resolved_uri) = file_info.attribute_as_string(TARGET_URI_ATTRIBUTE)
|
||||
{
|
||||
let resolved_uri = String::from(resolved_uri);
|
||||
let file = gio::File::for_uri(&resolved_uri);
|
||||
return (resolved_uri, file);
|
||||
}
|
||||
|
||||
(uri.to_string(), file)
|
||||
|
|
@ -60,7 +59,7 @@ fn items(monitor: &gio::VolumeMonitor, sizes: IconSizes) -> MounterItems {
|
|||
gio::Cancellable::NONE,
|
||||
)
|
||||
.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
|
||||
|
||||
MounterItem::Gvfs(Item {
|
||||
|
|
@ -457,9 +456,9 @@ impl Gvfs {
|
|||
log::info!("mount {name}: result {res:?}");
|
||||
// Update the mounter_item with mount information after successful mount
|
||||
let mut updated_item = mounter_item.clone();
|
||||
if res.is_ok() {
|
||||
if let MounterItem::Gvfs(ref mut item) = updated_item {
|
||||
if let Some(mount) = volume_for_callback.get_mount() {
|
||||
if res.is_ok()
|
||||
&& let MounterItem::Gvfs(ref mut item) = updated_item
|
||||
&& let Some(mount) = volume_for_callback.get_mount() {
|
||||
let root = MountExt::root(&mount);
|
||||
item.path_opt = root.path();
|
||||
item.is_mounted = true;
|
||||
|
|
@ -469,14 +468,9 @@ impl Gvfs {
|
|||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
|
||||
gio::Cancellable::NONE,
|
||||
)
|
||||
.ok()
|
||||
.and_then(|info| {
|
||||
Some(info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
||||
})
|
||||
.ok().map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
|
||||
.unwrap_or(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
event_tx.send(Event::MountResult(updated_item, match res {
|
||||
Ok(()) => {
|
||||
_ = complete_tx.send(Ok(()));
|
||||
|
|
|
|||
|
|
@ -246,19 +246,18 @@ struct State {
|
|||
|
||||
impl State {
|
||||
fn drag_rect(&self, cursor: mouse::Cursor) -> Option<Rectangle> {
|
||||
if let Some(drag_source) = self.drag_initiated {
|
||||
if let Some(position) = cursor.position().or(self.last_virtual_position) {
|
||||
if position.distance(drag_source) > 1.0 {
|
||||
let min_x = drag_source.x.min(position.x);
|
||||
let max_x = drag_source.x.max(position.x);
|
||||
let min_y = drag_source.y.min(position.y);
|
||||
let max_y = drag_source.y.max(position.y);
|
||||
return Some(Rectangle::new(
|
||||
Point::new(min_x, min_y),
|
||||
Size::new(max_x - min_x, max_y - min_y),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(drag_source) = self.drag_initiated
|
||||
&& let Some(position) = cursor.position().or(self.last_virtual_position)
|
||||
&& position.distance(drag_source) > 1.0
|
||||
{
|
||||
let min_x = drag_source.x.min(position.x);
|
||||
let max_x = drag_source.x.max(position.x);
|
||||
let min_y = drag_source.y.min(position.y);
|
||||
let max_y = drag_source.y.max(position.y);
|
||||
return Some(Rectangle::new(
|
||||
Point::new(min_x, min_y),
|
||||
Size::new(max_x - min_x, max_y - min_y),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -527,12 +526,12 @@ fn update<Message: Clone>(
|
|||
let offset = layout.virtual_offset();
|
||||
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 viewport_changed {
|
||||
shell.publish(message(*viewport));
|
||||
}
|
||||
if let Some(message) = widget.on_resize.as_ref()
|
||||
&& viewport_changed
|
||||
{
|
||||
shell.publish(message(*viewport));
|
||||
}
|
||||
|
||||
state.viewport = Some(*viewport);
|
||||
|
|
@ -664,113 +663,112 @@ fn update<Message: Clone>(
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_right_press.as_ref() {
|
||||
if matches!(
|
||||
if let Some(message) = widget.on_right_press.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
|
||||
) {
|
||||
let point_opt = if widget.on_right_press_window_position {
|
||||
cursor.position_over(layout_bounds).map(|mut p| {
|
||||
p.x -= offset.x;
|
||||
p.y -= offset.y;
|
||||
p
|
||||
})
|
||||
} else {
|
||||
cursor.position_in(layout_bounds)
|
||||
};
|
||||
shell.publish(message(point_opt));
|
||||
)
|
||||
{
|
||||
let point_opt = if widget.on_right_press_window_position {
|
||||
cursor.position_over(layout_bounds).map(|mut p| {
|
||||
p.x -= offset.x;
|
||||
p.y -= offset.y;
|
||||
p
|
||||
})
|
||||
} else {
|
||||
cursor.position_in(layout_bounds)
|
||||
};
|
||||
shell.publish(message(point_opt));
|
||||
|
||||
if widget.on_right_press_no_capture {
|
||||
return event::Status::Ignored;
|
||||
}
|
||||
return event::Status::Captured;
|
||||
if widget.on_right_press_no_capture {
|
||||
return event::Status::Ignored;
|
||||
}
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
if let Some(message) = widget.on_right_release.as_ref() {
|
||||
if matches!(
|
||||
if let Some(message) = widget.on_right_release.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_middle_press.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_middle_release.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_back_press.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_back_release.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_forward_press.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 matches!(
|
||||
if let Some(message) = widget.on_forward_release.as_ref()
|
||||
&& matches!(
|
||||
event,
|
||||
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 Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
|
||||
if let Some(message) = on_scroll(*delta) {
|
||||
shell.publish(message);
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
if let Some(on_scroll) = widget.on_scroll.as_ref()
|
||||
&& let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event
|
||||
&& let Some(message) = on_scroll(*delta)
|
||||
{
|
||||
shell.publish(message);
|
||||
return event::Status::Captured;
|
||||
}
|
||||
|
||||
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
spawn_detached::spawn_detached,
|
||||
tab,
|
||||
};
|
||||
use cosmic::iced::futures::{SinkExt, channel::mpsc::Sender};
|
||||
use cosmic::iced::futures::{self, SinkExt, StreamExt, channel::mpsc::Sender, stream};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::Formatter,
|
||||
|
|
@ -196,6 +196,31 @@ async fn copy_or_move(
|
|||
.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 {
|
||||
// List of compound extensions to check
|
||||
const COMPOUND_EXTENSIONS: &[&str] = &[
|
||||
|
|
@ -934,10 +959,10 @@ impl Operation {
|
|||
let dir_name = get_directory_name(file_name);
|
||||
let mut new_dir = to.join(dir_name);
|
||||
|
||||
if new_dir.exists() {
|
||||
if let Some(new_dir_parent) = new_dir.parent() {
|
||||
new_dir = copy_unique_path(&new_dir, new_dir_parent);
|
||||
}
|
||||
if new_dir.exists()
|
||||
&& let Some(new_dir_parent) = new_dir.parent()
|
||||
{
|
||||
new_dir = copy_unique_path(&new_dir, new_dir_parent);
|
||||
}
|
||||
|
||||
op_sel.ignored.push(path.clone());
|
||||
|
|
@ -1185,7 +1210,7 @@ mod tests {
|
|||
path::PathBuf,
|
||||
};
|
||||
|
||||
use cosmic::iced::futures::{StreamExt, channel::mpsc};
|
||||
use cosmic::iced::futures::{StreamExt, channel::mpsc, future};
|
||||
use log::debug;
|
||||
use test_log::test;
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ impl OpReader {
|
|||
|
||||
impl io::Read for OpReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
futures::executor::block_on(async {
|
||||
cosmic::iced::futures::executor::block_on(async {
|
||||
self.controller
|
||||
.check()
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::time::Instant;
|
|||
use std::{cell::Cell, error::Error, fs, ops::ControlFlow, path::PathBuf, rc::Rc};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::operation::OperationError;
|
||||
use crate::operation::{OperationError, sync_to_disk};
|
||||
|
||||
use super::{Controller, OperationSelection, ReplaceResult, copy_unique_path};
|
||||
|
||||
|
|
@ -57,6 +57,8 @@ impl Context {
|
|||
) -> Result<bool, OperationError> {
|
||||
let mut 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 {
|
||||
self.controller
|
||||
.check()
|
||||
|
|
@ -136,10 +138,13 @@ impl Context {
|
|||
}),
|
||||
is_cleanup: false,
|
||||
};
|
||||
if matches!(method, Method::Move { .. }) {
|
||||
if let Some(cleanup_op) = op.move_cleanup_op() {
|
||||
cleanup_ops.push(cleanup_op);
|
||||
}
|
||||
if matches!(method, Method::Move { .. })
|
||||
&& let Some(cleanup_op) = op.move_cleanup_op()
|
||||
{
|
||||
cleanup_ops.push(cleanup_op);
|
||||
}
|
||||
if let Some(parent) = op.to.parent() {
|
||||
target_dirs.insert(parent.to_path_buf());
|
||||
}
|
||||
ops.push(op);
|
||||
}
|
||||
|
|
@ -177,10 +182,19 @@ impl Context {
|
|||
&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
|
||||
if self.op_sel.ignored.contains(&op.from) {
|
||||
// So add the to path to the selection
|
||||
self.op_sel.selected.push(op.to.clone());
|
||||
self.op_sel.selected.push(op.to);
|
||||
}
|
||||
} else {
|
||||
// Cancelled
|
||||
|
|
@ -188,6 +202,9 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
// Flush files to disk
|
||||
sync_to_disk(written_files, target_dirs).await;
|
||||
|
||||
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 {
|
||||
compio::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
|
|
@ -411,8 +428,6 @@ impl Op {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
to_file.sync_all().await?;
|
||||
}
|
||||
OpKind::Move { cross_device_copy } => {
|
||||
// Remove `to` if overwriting and it is an existing file
|
||||
|
|
|
|||
659
src/tab.rs
659
src/tab.rs
|
|
@ -1,5 +1,8 @@
|
|||
use chrono::{Datelike, Timelike, Utc};
|
||||
use cosmic::{
|
||||
Apply, Element, cosmic_theme, font,
|
||||
Apply, Element, cosmic_theme,
|
||||
desktop::fde::{DesktopEntry, get_languages_from_env},
|
||||
font,
|
||||
iced::{
|
||||
Alignment,
|
||||
Border,
|
||||
|
|
@ -37,8 +40,6 @@ use cosmic::{
|
|||
menu::{action::MenuAction, key_bind::KeyBind},
|
||||
},
|
||||
};
|
||||
|
||||
use chrono::{Datelike, Timelike, Utc};
|
||||
use i18n_embed::LanguageLoader;
|
||||
use icu::{
|
||||
datetime::{
|
||||
|
|
@ -615,7 +616,8 @@ pub fn fs_kind(_metadata: &Metadata) -> FsKind {
|
|||
}
|
||||
|
||||
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,
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||
|
|
@ -623,14 +625,11 @@ fn get_desktop_file_display_name(path: &Path) -> Option<String> {
|
|||
}
|
||||
};
|
||||
|
||||
entry
|
||||
.section("Desktop Entry")
|
||||
.attr("Name")
|
||||
.map(str::to_string)
|
||||
entry.name(&locales).map(|s| s.into_owned())
|
||||
}
|
||||
|
||||
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,
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||
|
|
@ -638,10 +637,7 @@ fn get_desktop_file_icon(path: &Path) -> Option<String> {
|
|||
}
|
||||
};
|
||||
|
||||
entry
|
||||
.section("Desktop Entry")
|
||||
.attr("Icon")
|
||||
.map(str::to_string)
|
||||
entry.icon().map(str::to_string)
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
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,
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||
return (None, None);
|
||||
}
|
||||
};
|
||||
let section = entry.section("Desktop Entry");
|
||||
(
|
||||
section.attr("Name").map(str::to_string),
|
||||
section.attr("Icon").map(str::to_string),
|
||||
entry.name(&locales).map(|s| s.into_owned()),
|
||||
entry.icon().map(str::to_string),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -934,43 +930,43 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
|||
|
||||
#[cfg(feature = "gvfs")]
|
||||
{
|
||||
if let Ok(path_meta) = fs::metadata(tab_path) {
|
||||
if fs_kind(&path_meta) == FsKind::Gvfs {
|
||||
let file = gio::File::for_path(tab_path);
|
||||
if let Ok(path_meta) = fs::metadata(tab_path)
|
||||
&& fs_kind(&path_meta) == FsKind::Gvfs
|
||||
{
|
||||
let file = gio::File::for_path(tab_path);
|
||||
|
||||
// gio crate expects a comma delimited string
|
||||
let attr_string = [
|
||||
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),
|
||||
]
|
||||
.join(",");
|
||||
// gio crate expects a comma delimited string
|
||||
let attr_string = [
|
||||
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(),
|
||||
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_TIME_MODIFIED.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
|
||||
gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),
|
||||
]
|
||||
.join(",");
|
||||
|
||||
match gio::prelude::FileExt::enumerate_children(
|
||||
&file,
|
||||
attr_string.as_str(),
|
||||
gio::FileQueryInfoFlags::NONE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(res) => {
|
||||
remote_scannable = true;
|
||||
items = res
|
||||
.filter_map(|file| {
|
||||
let file = file.ok()?;
|
||||
Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"could not enumerate {} via gio: {}",
|
||||
tab_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
match gio::prelude::FileExt::enumerate_children(
|
||||
&file,
|
||||
attr_string.as_str(),
|
||||
gio::FileQueryInfoFlags::NONE,
|
||||
gio::Cancellable::NONE,
|
||||
) {
|
||||
Ok(res) => {
|
||||
remote_scannable = true;
|
||||
items = res
|
||||
.filter_map(|file| {
|
||||
let file = file.ok()?;
|
||||
Some(item_from_gvfs_info(tab_path.join(file.name()), file, sizes))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"could not enumerate {} via gio: {}",
|
||||
tab_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1410,11 +1406,11 @@ impl EditLocation {
|
|||
self.selected = Some(selected);
|
||||
|
||||
// Automatically resolve if there is only one completion
|
||||
if completions.len() == 1 {
|
||||
if let Some(resolved) = self.resolve() {
|
||||
self.location = resolved;
|
||||
self.selected = None;
|
||||
}
|
||||
if completions.len() == 1
|
||||
&& let Some(resolved) = self.resolve()
|
||||
{
|
||||
self.location = resolved;
|
||||
self.selected = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -2033,10 +2029,10 @@ impl ItemThumbnail {
|
|||
if let Some((item_thumbnail, temp_file)) =
|
||||
Self::generate_thumbnail_external(path, &mime, thumbnail_size, thumbnail_dir)
|
||||
{
|
||||
if let Ok(cache) = thumbnail_cacher {
|
||||
if let Err(err) = cache.update_with_temp_file(temp_file) {
|
||||
log::warn!("failed to update cache for {}: {}", path.display(), err);
|
||||
}
|
||||
if let Ok(cache) = thumbnail_cacher
|
||||
&& let Err(err) = cache.update_with_temp_file(temp_file)
|
||||
{
|
||||
log::warn!("failed to update cache for {}: {}", path.display(), err);
|
||||
}
|
||||
return item_thumbnail;
|
||||
}
|
||||
|
|
@ -2076,16 +2072,15 @@ impl ItemThumbnail {
|
|||
// 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
|
||||
// next time.
|
||||
if let Ok(cacher) = thumbnail_cacher {
|
||||
if tried_supported_file {
|
||||
if let Err(err) = cacher.create_fail_marker() {
|
||||
log::warn!(
|
||||
"failed to create thumbnail fail marker for {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Ok(cacher) = thumbnail_cacher
|
||||
&& tried_supported_file
|
||||
&& let Err(err) = cacher.create_fail_marker()
|
||||
{
|
||||
log::warn!(
|
||||
"failed to create thumbnail fail marker for {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
Self::NotImage
|
||||
|
|
@ -2273,13 +2268,13 @@ impl Item {
|
|||
widget::button::icon(widget::icon::from_name("go-next-symbolic"))
|
||||
.on_press(Message::ItemRight),
|
||||
);
|
||||
if self.can_gallery() {
|
||||
if let Some(_path) = self.path_opt() {
|
||||
row = row.push(
|
||||
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
|
||||
.on_press(Message::Gallery(true)),
|
||||
);
|
||||
}
|
||||
if self.can_gallery()
|
||||
&& let Some(_path) = self.path_opt()
|
||||
{
|
||||
row = row.push(
|
||||
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
|
||||
.on_press(Message::Gallery(true)),
|
||||
);
|
||||
}
|
||||
row.into()
|
||||
}
|
||||
|
|
@ -2445,18 +2440,20 @@ impl Item {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(path) = self.path_opt() {
|
||||
if let Ok(img) = image::image_dimensions(path) {
|
||||
let (width, height) = img;
|
||||
details = details.push(widget::text::body(format!("{width}x{height}")));
|
||||
}
|
||||
if let Some(path) = self.path_opt()
|
||||
&& let Ok(img) = image::image_dimensions(path)
|
||||
{
|
||||
let (width, height) = img;
|
||||
details = details.push(widget::text::body(format!("{width}x{height}")));
|
||||
}
|
||||
column = column.push(details);
|
||||
|
||||
if let Some(path) = self.path_opt() {
|
||||
column = column.push(
|
||||
widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))),
|
||||
);
|
||||
if self.selected {
|
||||
column = column.push(
|
||||
widget::button::standard(fl!("open")).on_press(Message::Open(Some(path.clone()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !settings.is_empty() {
|
||||
|
|
@ -2635,12 +2632,11 @@ async fn calculate_dir_size(path: &Path, controller: Controller) -> Result<u64,
|
|||
.map_err(|s| OperationError::from_state(s, &controller))?;
|
||||
|
||||
//TODO: report more errors?
|
||||
if let Ok(entry) = entry_res {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if metadata.is_file() {
|
||||
total += metadata.len();
|
||||
}
|
||||
}
|
||||
if let Ok(entry) = entry_res
|
||||
&& let Ok(metadata) = entry.metadata()
|
||||
&& metadata.is_file()
|
||||
{
|
||||
total += metadata.len();
|
||||
}
|
||||
|
||||
// Yield in case this process takes a while.
|
||||
|
|
@ -2766,10 +2762,10 @@ impl Tab {
|
|||
let selected = self.selected_locations();
|
||||
for item in &mut items {
|
||||
item.selected = false;
|
||||
if let Some(location) = &item.location_opt {
|
||||
if selected.contains(location) {
|
||||
item.selected = true;
|
||||
}
|
||||
if let Some(location) = &item.location_opt
|
||||
&& selected.contains(location)
|
||||
{
|
||||
item.selected = true;
|
||||
}
|
||||
}
|
||||
self.items_opt = Some(items);
|
||||
|
|
@ -2788,10 +2784,9 @@ impl Tab {
|
|||
for item in items.iter_mut() {
|
||||
item.cut = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2881,11 +2876,11 @@ impl Tab {
|
|||
if let Some(ref mut items) = self.items_opt {
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
item.selected = false;
|
||||
if let Some(path) = item.path_opt() {
|
||||
if paths.contains(path) {
|
||||
item.selected = true;
|
||||
self.select_focus = Some(i);
|
||||
}
|
||||
if let Some(path) = item.path_opt()
|
||||
&& paths.contains(path)
|
||||
{
|
||||
item.selected = true;
|
||||
self.select_focus = Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3085,10 +3080,10 @@ impl Tab {
|
|||
return Vec::new();
|
||||
};
|
||||
|
||||
if let Some((w, h)) = original_dims {
|
||||
if !should_use_tiling(*w, *h) {
|
||||
return Vec::new();
|
||||
}
|
||||
if let Some((w, h)) = original_dims
|
||||
&& !should_use_tiling(*w, *h)
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(path) = item.path_opt() else {
|
||||
|
|
@ -3389,15 +3384,13 @@ impl Tab {
|
|||
self.date_time_formatter = date_time_formatter(self.config.military_time);
|
||||
self.time_formatter = time_formatter(self.config.military_time);
|
||||
}
|
||||
if show_hidden_changed {
|
||||
if let Location::Search(path, term, ..) = &self.location {
|
||||
cd = Some(Location::Search(
|
||||
path.clone(),
|
||||
term.clone(),
|
||||
self.config.show_hidden,
|
||||
Instant::now(),
|
||||
));
|
||||
}
|
||||
if show_hidden_changed && let Location::Search(path, term, ..) = &self.location {
|
||||
cd = Some(Location::Search(
|
||||
path.clone(),
|
||||
term.clone(),
|
||||
self.config.show_hidden,
|
||||
Instant::now(),
|
||||
));
|
||||
}
|
||||
// Unhighlight all items when config changes
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
|
|
@ -3418,11 +3411,12 @@ impl Tab {
|
|||
self.location_context_menu_index = None;
|
||||
|
||||
//TODO: hack for clearing selecting when right clicking empty space
|
||||
if self.context_menu.is_some() && self.last_right_click.take().is_none() {
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
item.selected = false;
|
||||
}
|
||||
if self.context_menu.is_some()
|
||||
&& self.last_right_click.take().is_none()
|
||||
&& let Some(ref mut items) = self.items_opt
|
||||
{
|
||||
for item in items.iter_mut() {
|
||||
item.selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3510,11 +3504,11 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::EditLocationComplete(selected) => {
|
||||
if let Some(mut edit_location) = self.edit_location.take() {
|
||||
if !matches!(edit_location.location, Location::Network(..)) {
|
||||
edit_location.selected = Some(selected);
|
||||
cd = edit_location.resolve();
|
||||
}
|
||||
if let Some(mut edit_location) = self.edit_location.take()
|
||||
&& !matches!(edit_location.location, Location::Network(..))
|
||||
{
|
||||
edit_location.selected = Some(selected);
|
||||
cd = edit_location.resolve();
|
||||
}
|
||||
}
|
||||
Message::EditLocationEnable => {
|
||||
|
|
@ -3530,11 +3524,11 @@ impl Tab {
|
|||
&& edit_location
|
||||
.completions
|
||||
.as_ref()
|
||||
.map_or(false, |completions| !completions.is_empty())
|
||||
.is_some_and(|completions| !completions.is_empty())
|
||||
&& edit_location
|
||||
.location
|
||||
.path_opt()
|
||||
.map_or(false, |path| !path.exists())
|
||||
.is_some_and(|path| !path.exists())
|
||||
{
|
||||
edit_location.selected = Some(0);
|
||||
}
|
||||
|
|
@ -3632,19 +3626,19 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::GoNext => {
|
||||
if let Some(history_i) = self.history_i.checked_add(1) {
|
||||
if let Some(location) = self.history.get(history_i) {
|
||||
cd = Some(location.clone());
|
||||
history_i_opt = Some(history_i);
|
||||
}
|
||||
if let Some(history_i) = self.history_i.checked_add(1)
|
||||
&& let Some(location) = self.history.get(history_i)
|
||||
{
|
||||
cd = Some(location.clone());
|
||||
history_i_opt = Some(history_i);
|
||||
}
|
||||
}
|
||||
Message::GoPrevious => {
|
||||
if let Some(history_i) = self.history_i.checked_sub(1) {
|
||||
if let Some(location) = self.history.get(history_i) {
|
||||
cd = Some(location.clone());
|
||||
history_i_opt = Some(history_i);
|
||||
}
|
||||
if let Some(history_i) = self.history_i.checked_sub(1)
|
||||
&& let Some(location) = self.history.get(history_i)
|
||||
{
|
||||
cd = Some(location.clone());
|
||||
history_i_opt = Some(history_i);
|
||||
}
|
||||
}
|
||||
Message::ItemDown => {
|
||||
|
|
@ -3823,10 +3817,10 @@ impl Tab {
|
|||
Message::LocationUp => {
|
||||
// Sets location to the path's parent
|
||||
// Does nothing if path is root or location is Trash
|
||||
if let Location::Path(ref path) = self.location {
|
||||
if let Some(parent) = path.parent() {
|
||||
cd = Some(Location::Path(parent.to_owned()));
|
||||
}
|
||||
if let Location::Path(ref path) = self.location
|
||||
&& let Some(parent) = path.parent()
|
||||
{
|
||||
cd = Some(Location::Path(parent.to_owned()));
|
||||
}
|
||||
}
|
||||
Message::Open(path_opt) => {
|
||||
|
|
@ -3868,27 +3862,25 @@ impl Tab {
|
|||
match mode {
|
||||
Mode::App => {
|
||||
if is_only_one_selected {
|
||||
return ResolveResult::Cd(location.clone());
|
||||
ResolveResult::Cd(location.clone())
|
||||
} else {
|
||||
return ResolveResult::OpenInTab(path_opt.cloned());
|
||||
ResolveResult::OpenInTab(path_opt.cloned())
|
||||
}
|
||||
}
|
||||
Mode::Desktop => {
|
||||
return match location {
|
||||
Location::Trash => ResolveResult::OpenTrash,
|
||||
_ => ResolveResult::Open(path_opt.cloned()),
|
||||
};
|
||||
}
|
||||
Mode::Desktop => match location {
|
||||
Location::Trash => ResolveResult::OpenTrash,
|
||||
_ => ResolveResult::Open(path_opt.cloned()),
|
||||
},
|
||||
Mode::Dialog(_) => {
|
||||
if is_only_one_selected {
|
||||
return ResolveResult::Cd(location.clone());
|
||||
ResolveResult::Cd(location.clone())
|
||||
} else {
|
||||
return ResolveResult::Skip;
|
||||
ResolveResult::Skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ResolveResult::Open(path_opt.cloned());
|
||||
ResolveResult::Open(path_opt.cloned())
|
||||
}
|
||||
}
|
||||
let mut open_files = Vec::new();
|
||||
|
|
@ -3933,14 +3925,13 @@ impl Tab {
|
|||
if mod_ctrl || mod_shift {
|
||||
self.update(Message::Click(click_i_opt), modifiers);
|
||||
}
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
if !click_i_opt
|
||||
if let Some(ref mut items) = self.items_opt
|
||||
&& !click_i_opt
|
||||
.is_some_and(|click_i| items.get(click_i).is_some_and(|x| x.selected))
|
||||
{
|
||||
// If item not selected, clear selection on other items
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
item.selected = Some(i) == click_i_opt;
|
||||
}
|
||||
{
|
||||
// If item not selected, clear selection on other items
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
item.selected = Some(i) == click_i_opt;
|
||||
}
|
||||
}
|
||||
//TODO: hack for clearing selecting when right clicking empty space
|
||||
|
|
@ -3988,12 +3979,12 @@ impl Tab {
|
|||
}
|
||||
Message::Resize(viewport) => {
|
||||
// Scroll to ensure focused item still in view
|
||||
if self.viewport_opt.map(|v| v.size()) != Some(viewport.size()) {
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Iced(
|
||||
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
|
||||
));
|
||||
}
|
||||
if self.viewport_opt.map(|v| v.size()) != Some(viewport.size())
|
||||
&& let Some(offset) = self.select_focus_scroll()
|
||||
{
|
||||
commands.push(Command::Iced(
|
||||
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
|
||||
));
|
||||
}
|
||||
|
||||
self.viewport_opt = Some(viewport);
|
||||
|
|
@ -4097,20 +4088,17 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::SelectLast => {
|
||||
if let Some(ref items) = self.items_opt {
|
||||
if let Some(last_pos) = items.iter().filter_map(|item| item.pos_opt.get()).max()
|
||||
{
|
||||
if self.select_position(last_pos.0, last_pos.1, mod_shift) {
|
||||
if let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Iced(
|
||||
scrollable::scroll_to(self.scrollable_id.clone(), offset)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::Iced(widget::button::focus(id).into()));
|
||||
}
|
||||
}
|
||||
if let Some(ref items) = self.items_opt
|
||||
&& 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 let Some(offset) = self.select_focus_scroll() {
|
||||
commands.push(Command::Iced(
|
||||
scrollable::scroll_to(self.scrollable_id.clone(), offset).into(),
|
||||
));
|
||||
}
|
||||
if let Some(id) = self.select_focus_id() {
|
||||
commands.push(Command::Iced(widget::button::focus(id).into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4134,13 +4122,13 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
Message::TabComplete(path, completions) => {
|
||||
if let Some(edit_location) = &mut self.edit_location {
|
||||
if edit_location.location.path_opt() == Some(&path) {
|
||||
edit_location.completions = Some(completions);
|
||||
commands.push(Command::Iced(
|
||||
widget::text_input::focus(self.edit_location_id.clone()).into(),
|
||||
));
|
||||
}
|
||||
if let Some(edit_location) = &mut self.edit_location
|
||||
&& edit_location.location.path_opt() == Some(&path)
|
||||
{
|
||||
edit_location.completions = Some(completions);
|
||||
commands.push(Command::Iced(
|
||||
widget::text_input::focus(self.edit_location_id.clone()).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Message::Thumbnail(path, thumbnail) => {
|
||||
|
|
@ -4274,10 +4262,10 @@ impl Tab {
|
|||
}
|
||||
Message::DirectorySize(path, dir_size) => {
|
||||
let location = Location::Path(path);
|
||||
if let Some(ref mut item) = self.parent_item_opt {
|
||||
if item.location_opt.as_ref() == Some(&location) {
|
||||
item.dir_size.clone_from(&dir_size);
|
||||
}
|
||||
if let Some(ref mut item) = self.parent_item_opt
|
||||
&& item.location_opt.as_ref() == Some(&location)
|
||||
{
|
||||
item.dir_size.clone_from(&dir_size);
|
||||
}
|
||||
if let Some(ref mut items) = self.items_opt {
|
||||
for item in items.iter_mut() {
|
||||
|
|
@ -4315,13 +4303,12 @@ impl Tab {
|
|||
} else {
|
||||
// Select parent if location is not directory
|
||||
let mut selected_paths = None;
|
||||
if let Some(path) = location.path_opt() {
|
||||
if !path.is_dir() {
|
||||
if let Some(parent) = path.parent() {
|
||||
selected_paths = Some(vec![path.clone()]);
|
||||
location = location.with_path(parent.to_path_buf());
|
||||
}
|
||||
}
|
||||
if let Some(path) = location.path_opt()
|
||||
&& !path.is_dir()
|
||||
&& let Some(parent) = path.parent()
|
||||
{
|
||||
selected_paths = Some(vec![path.clone()]);
|
||||
location = location.with_path(parent.to_path_buf());
|
||||
}
|
||||
if location != self.location || selected_paths.is_some() {
|
||||
if location.path_opt().is_none_or(|path| path.is_dir()) {
|
||||
|
|
@ -4529,99 +4516,93 @@ impl Tab {
|
|||
//TODO: display error messages when image not found?
|
||||
let mut name_opt = None;
|
||||
let mut element_opt: Option<Element<Message>> = None;
|
||||
if let Some(index) = self.select_focus {
|
||||
if let Some(items) = &self.items_opt {
|
||||
if let Some(item) = items.get(index) {
|
||||
name_opt = Some(widget::text::heading(&item.display_name));
|
||||
match item
|
||||
.thumbnail_opt
|
||||
.as_ref()
|
||||
.unwrap_or(&ItemThumbnail::NotImage)
|
||||
{
|
||||
ItemThumbnail::NotImage => {}
|
||||
ItemThumbnail::Image(handle, original_dims) => {
|
||||
// Determine which image to show based on async decode state
|
||||
let mut is_loading = false;
|
||||
let mut error_msg_opt = None;
|
||||
let image_handle = if let Some(path) = item.path_opt() {
|
||||
if let Some(error_msg) = self.large_image_manager.get_error(path) {
|
||||
error_msg_opt = Some(error_msg.clone());
|
||||
handle.clone()
|
||||
} else if self.large_image_manager.is_decoding(path) {
|
||||
// Currently decoding (initial or re-decode) --> show cached/thumbnail with loading indicator
|
||||
is_loading = true;
|
||||
// Use decoded handle if available (re-decode), otherwise thumbnail (initial decode)
|
||||
self.large_image_manager
|
||||
.get_decoded(path)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| handle.clone())
|
||||
} else if let Some(decoded_handle) =
|
||||
self.large_image_manager.get_decoded(path)
|
||||
{
|
||||
// Decoded and not currently decoding --> use it
|
||||
decoded_handle.clone()
|
||||
} else if let Some((w, h)) = original_dims {
|
||||
// Check if image needs tiling
|
||||
if should_use_tiling(*w, *h) {
|
||||
// Large image --> show thumbnail only
|
||||
handle.clone()
|
||||
} else {
|
||||
// Normal-sized image --> load full resolution directly
|
||||
widget::image::Handle::from_path(path)
|
||||
}
|
||||
} else {
|
||||
// No dimensions available --> show thumbnail
|
||||
handle.clone()
|
||||
}
|
||||
} else {
|
||||
if let Some(index) = self.select_focus
|
||||
&& let Some(items) = &self.items_opt
|
||||
&& let Some(item) = items.get(index)
|
||||
{
|
||||
name_opt = Some(widget::text::heading(&item.display_name));
|
||||
match item
|
||||
.thumbnail_opt
|
||||
.as_ref()
|
||||
.unwrap_or(&ItemThumbnail::NotImage)
|
||||
{
|
||||
ItemThumbnail::NotImage => {}
|
||||
ItemThumbnail::Image(handle, original_dims) => {
|
||||
// Determine which image to show based on async decode state
|
||||
let mut is_loading = false;
|
||||
let mut error_msg_opt = None;
|
||||
let image_handle = if let Some(path) = item.path_opt() {
|
||||
if let Some(error_msg) = self.large_image_manager.get_error(path) {
|
||||
error_msg_opt = Some(error_msg.clone());
|
||||
handle.clone()
|
||||
} else if self.large_image_manager.is_decoding(path) {
|
||||
// Currently decoding (initial or re-decode) --> show cached/thumbnail with loading indicator
|
||||
is_loading = true;
|
||||
// Use decoded handle if available (re-decode), otherwise thumbnail (initial decode)
|
||||
self.large_image_manager
|
||||
.get_decoded(path)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| handle.clone())
|
||||
} else if let Some(decoded_handle) =
|
||||
self.large_image_manager.get_decoded(path)
|
||||
{
|
||||
// Decoded and not currently decoding --> use it
|
||||
decoded_handle.clone()
|
||||
} else if let Some((w, h)) = original_dims {
|
||||
// Check if image needs tiling
|
||||
if should_use_tiling(*w, *h) {
|
||||
// Large image --> show thumbnail only
|
||||
handle.clone()
|
||||
};
|
||||
} else {
|
||||
// Normal-sized image --> load full resolution directly
|
||||
widget::image::Handle::from_path(path)
|
||||
}
|
||||
} else {
|
||||
// No dimensions available --> show thumbnail
|
||||
handle.clone()
|
||||
}
|
||||
} else {
|
||||
handle.clone()
|
||||
};
|
||||
|
||||
let content: cosmic::Element<'_, Message> =
|
||||
if let Some(error_msg) = error_msg_opt {
|
||||
widget::column()
|
||||
.push(widget::image(image_handle))
|
||||
.push(widget::text(format!("⚠ {}", error_msg)).size(13))
|
||||
.padding(space_xs)
|
||||
.align_x(cosmic::iced::Alignment::Center)
|
||||
.into()
|
||||
} else if is_loading {
|
||||
widget::column()
|
||||
.push(widget::image(image_handle))
|
||||
.push(widget::text("Loading higher resolution...").size(14))
|
||||
.padding(space_xs)
|
||||
.align_x(cosmic::iced::Alignment::Center)
|
||||
.into()
|
||||
} else {
|
||||
//TODO: use widget::image::viewer, when its zoom can be reset
|
||||
widget::image(image_handle).into()
|
||||
};
|
||||
let content: cosmic::Element<'_, Message> =
|
||||
if let Some(error_msg) = error_msg_opt {
|
||||
widget::column()
|
||||
.push(widget::image(image_handle))
|
||||
.push(widget::text(format!("⚠ {}", error_msg)).size(13))
|
||||
.padding(space_xs)
|
||||
.align_x(cosmic::iced::Alignment::Center)
|
||||
.into()
|
||||
} else if is_loading {
|
||||
widget::column()
|
||||
.push(widget::image(image_handle))
|
||||
.push(widget::text("Loading higher resolution...").size(14))
|
||||
.padding(space_xs)
|
||||
.align_x(cosmic::iced::Alignment::Center)
|
||||
.into()
|
||||
} else {
|
||||
//TODO: use widget::image::viewer, when its zoom can be reset
|
||||
widget::image(image_handle).into()
|
||||
};
|
||||
|
||||
element_opt =
|
||||
Some(widget::container(content).center(Length::Fill).into());
|
||||
}
|
||||
ItemThumbnail::Svg(handle) => {
|
||||
element_opt = Some(
|
||||
widget::svg(handle.clone())
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ItemThumbnail::Text(text) => {
|
||||
element_opt = Some(
|
||||
widget::container(
|
||||
widget::text_editor(text).padding(space_xxs).class(
|
||||
cosmic::theme::iced::TextEditor::Custom(Box::new(
|
||||
text_editor_class,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.center(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
element_opt = Some(widget::container(content).center(Length::Fill).into());
|
||||
}
|
||||
ItemThumbnail::Svg(handle) => {
|
||||
element_opt = Some(
|
||||
widget::svg(handle.clone())
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ItemThumbnail::Text(text) => {
|
||||
element_opt = Some(
|
||||
widget::container(widget::text_editor(text).padding(space_xxs).class(
|
||||
cosmic::theme::iced::TextEditor::Custom(Box::new(text_editor_class)),
|
||||
))
|
||||
.center(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4860,32 +4841,32 @@ impl Tab {
|
|||
);
|
||||
let mut popover =
|
||||
widget::popover(text_input).position(widget::popover::Position::Bottom);
|
||||
if let Some(completions) = &edit_location.completions {
|
||||
if !completions.is_empty() {
|
||||
let mut column =
|
||||
widget::column::with_capacity(completions.len()).padding(space_xxs);
|
||||
for (i, (name, _path)) in completions.iter().enumerate() {
|
||||
let selected = edit_location.selected == Some(i);
|
||||
column = column.push(
|
||||
widget::button::custom(widget::text::body(name))
|
||||
//TODO: match to design
|
||||
.class(if selected {
|
||||
theme::Button::Standard
|
||||
} else {
|
||||
theme::Button::HeaderBar
|
||||
})
|
||||
.on_press(Message::EditLocationComplete(i))
|
||||
.padding(space_xxs)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
}
|
||||
popover = popover.popup(
|
||||
widget::container(column)
|
||||
.class(theme::Container::Dropdown)
|
||||
//TODO: This is a hack to get the popover to be the right width
|
||||
.max_width(size.width - 140.0),
|
||||
if let Some(completions) = &edit_location.completions
|
||||
&& !completions.is_empty()
|
||||
{
|
||||
let mut column =
|
||||
widget::column::with_capacity(completions.len()).padding(space_xxs);
|
||||
for (i, (name, _path)) in completions.iter().enumerate() {
|
||||
let selected = edit_location.selected == Some(i);
|
||||
column = column.push(
|
||||
widget::button::custom(widget::text::body(name))
|
||||
//TODO: match to design
|
||||
.class(if selected {
|
||||
theme::Button::Standard
|
||||
} else {
|
||||
theme::Button::HeaderBar
|
||||
})
|
||||
.on_press(Message::EditLocationComplete(i))
|
||||
.padding(space_xxs)
|
||||
.width(Length::Fill),
|
||||
);
|
||||
}
|
||||
popover = popover.popup(
|
||||
widget::container(column)
|
||||
.class(theme::Container::Dropdown)
|
||||
//TODO: This is a hack to get the popover to be the right width
|
||||
.max_width(size.width - 140.0),
|
||||
);
|
||||
}
|
||||
row = row.push(popover);
|
||||
let mut column = widget::column::with_capacity(4).padding([0, space_s]);
|
||||
|
|
@ -5911,13 +5892,13 @@ impl Tab {
|
|||
.wayland_on_right_press_window_position();
|
||||
|
||||
let mut popover = widget::popover(mouse_area);
|
||||
if let Some(point) = self.context_menu {
|
||||
if !cfg!(feature = "wayland") || !crate::is_wayland() {
|
||||
let context_menu = menu::context_menu(self, key_binds, &modifiers);
|
||||
popover = popover
|
||||
.popup(context_menu)
|
||||
.position(widget::popover::Position::Point(point));
|
||||
}
|
||||
if let Some(point) = self.context_menu
|
||||
&& (!cfg!(feature = "wayland") || !crate::is_wayland())
|
||||
{
|
||||
let context_menu = menu::context_menu(self, key_binds, modifiers);
|
||||
popover = popover
|
||||
.popup(context_menu)
|
||||
.position(widget::popover::Position::Point(point));
|
||||
}
|
||||
|
||||
let mut tab_column = widget::column::with_capacity(3);
|
||||
|
|
@ -5937,21 +5918,21 @@ impl Tab {
|
|||
}
|
||||
match &self.location {
|
||||
Location::Trash => {
|
||||
if let Some(items) = self.items_opt() {
|
||||
if !items.is_empty() {
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children([
|
||||
widget::horizontal_space().into(),
|
||||
widget::button::standard(fl!("empty-trash"))
|
||||
.on_press(Message::EmptyTrash)
|
||||
.into(),
|
||||
]))
|
||||
.padding([space_xxs, space_xs])
|
||||
.layer(cosmic_theme::Layer::Primary)
|
||||
.apply(widget::container)
|
||||
.padding([0, 0, 7, 0]),
|
||||
);
|
||||
}
|
||||
if let Some(items) = self.items_opt()
|
||||
&& !items.is_empty()
|
||||
{
|
||||
tab_column = tab_column.push(
|
||||
widget::layer_container(widget::row::with_children([
|
||||
widget::horizontal_space().into(),
|
||||
widget::button::standard(fl!("empty-trash"))
|
||||
.on_press(Message::EmptyTrash)
|
||||
.into(),
|
||||
]))
|
||||
.padding([space_xxs, space_xs])
|
||||
.layer(cosmic_theme::Layer::Primary)
|
||||
.apply(widget::container)
|
||||
.padding([0, 0, 7, 0]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Location::Network(uri, _display_name, _path) if uri == "network:///" => {
|
||||
|
|
@ -6248,10 +6229,10 @@ impl Tab {
|
|||
let mut selected_items: Vec<&Item> =
|
||||
items.iter().filter(|item| item.selected).collect();
|
||||
|
||||
if selected_items.is_empty() {
|
||||
if let Some(p) = self.parent_item_opt.as_ref() {
|
||||
selected_items.push(p)
|
||||
}
|
||||
if selected_items.is_empty()
|
||||
&& let Some(p) = self.parent_item_opt.as_ref()
|
||||
{
|
||||
selected_items.push(p)
|
||||
}
|
||||
for item in selected_items {
|
||||
// Item must have a path
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ impl ThumbnailCacher {
|
|||
if let (Some(cache_base_dir), Ok(metadata)) = (
|
||||
THUMBNAIL_CACHE_BASE_DIR.as_ref(),
|
||||
std::fs::metadata(&self.file_path),
|
||||
) {
|
||||
if metadata.is_file() && self.file_path.starts_with(cache_base_dir) {
|
||||
return CachedThumbnail::Valid((self.file_path.clone(), None));
|
||||
}
|
||||
) && metadata.is_file()
|
||||
&& self.file_path.starts_with(cache_base_dir)
|
||||
{
|
||||
return CachedThumbnail::Valid((self.file_path.clone(), None));
|
||||
}
|
||||
|
||||
// Use cached thumbnail if it is valid.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::desktop::fde::GenericEntry;
|
||||
use mime_guess::Mime;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
|
|
@ -115,7 +116,7 @@ impl ThumbnailerCache {
|
|||
|
||||
//TODO: handle directory specific behavior
|
||||
for path in thumbnailer_paths {
|
||||
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
||||
let entry = match GenericEntry::from_path(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse {}: {}", path.display(), err);
|
||||
|
|
@ -124,12 +125,18 @@ impl ThumbnailerCache {
|
|||
};
|
||||
|
||||
//TODO: use TryExec?
|
||||
let section = entry.section("Thumbnailer Entry");
|
||||
let Some(exec) = section.attr("Exec") else {
|
||||
let Some(section) = entry.group("Thumbnailer Entry") 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());
|
||||
continue;
|
||||
};
|
||||
let Some(mime_types) = section.attr("MimeType") else {
|
||||
let Some(mime_types) = section.entry("MimeType") else {
|
||||
log::warn!(
|
||||
"missing MimeType attribute for thumbnailer {}",
|
||||
path.display()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue