Compare commits

...

80 commits

Author SHA1 Message Date
20842026d7 chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
4b5fcfe5d7 merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
#	src/menu.rs
2026-09-24 20:51:53 +02:00
Hojjat
d1f0af78e6 chore: update libcosmic to fix esc on context-menu 2026-09-23 10:44:56 -06:00
Hojjat
87f407e3ed chore: update libcosmic to fix shift+right-click 2026-09-22 10:08:24 -06:00
Huy Nguyen
e2cad979ce
feat(dialog): persist global sort option (#2039)
Add dialog_sort_name field to State to track the dialog sort option.
Add state_handler and state fields to Dialog to load saved state.
Implement tab::Command::SetSort for Dialog to apply and persist sort
options.
2026-09-22 10:06:06 -06:00
Jeremy Soller
b326e48cf6
Make Cosmic Files show .exe icons (#2070)
* [Feature Request] Make Cosmic Files show .exe icons like Dolphin

* Sped Up Icon Extraction

* Compacted Code

* Replace wrestool with pelite crate for .exe icon extraction

* Run PE icon extraction as external thumbnailer

* Use dedicated external EXE thumbnailer

* Remove embedded EXE thumbnailer

* Move cosmic-files-thumbnailer to new crate

---------

Co-authored-by: ZlordHUN <137647604+ZlordHUN@users.noreply.github.com>
2026-09-21 13:25:53 -06:00
Jason Rodney Hansen
c0b8a8c60a fix: drag selection
Fixes bug where drag selection only worked if the pointer passed through an
item.
2026-09-21 13:09:39 -06:00
Michael Murphy
fe71aca0e5
i18n: translation update from Hosted Weblate (#2040)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Files](https://hosted.weblate.org/projects/pop-os/cosmic-files/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-files/matrix-auto.svg)
2026-09-15 21:51:43 +02:00
Michael Aaron Murphy
d6e7618b76 chore: update pinned cosmic-protocols 2026-09-15 18:33:17 +02:00
Michael Aaron Murphy
c576bbcd60 Epoch 1.9.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-15 18:33:17 +02:00
Hojjat
762fc8fb48 chore: update libcosmic & cargo +nightly fmt 2026-09-14 20:25:48 +02:00
Hojjat
7ba55f66cc chore: adopt typed surface actions from libcosmic 2026-09-14 20:25:48 +02:00
Hojjat
b7005660f1 feat: use libcosmic's context_menu widget for the breadcrumb menu 2026-09-14 20:25:48 +02:00
Hojjat
77f062647c feat: use libcosmic's context_menu widget for the file context menu 2026-09-14 20:25:48 +02:00
Hosted Weblate
00233428b3
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Satakun Utama <galaxytomostore@gmail.com>
Co-authored-by: Thomas Worofsky <thomas.worofsky@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/th/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-09-13 01:56:28 +02:00
98159d317c chore: fige Cargo.lock 2026-09-10 18:12:37 +02:00
e8eb42b507 Merge tag 'epoch-1.8.0' into local/libcosmic-path-patch
Epoch 1.8.0

# Conflicts:
#	Cargo.lock
2026-09-10 17:57:28 +02:00
dd57647ced chore: fige Cargo.lock (reordonnancement patch.unused par cargo) 2026-09-04 10:48:53 +02:00
Levi Portenier
089ad2b417
Epoch 1.8.0 version update (#2036)
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 13:02:27 -06:00
Jeremy Soller
1761b9b84b Epoch 1.8.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 08:14:58 -06:00
Josh Megnauth
06278ee7f1 Change focus only if selected tab is closed
Closes: #1921

Closing a tab other than the focused tab shouldn't change the focus.
2026-09-01 14:29:34 -06:00
Michael Murphy
a412787d69
i18n: translation update from Hosted Weblate (#2016)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Files](https://hosted.weblate.org/projects/pop-os/cosmic-files/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-files/matrix-auto.svg)
2026-09-01 19:45:10 +02:00
Hosted Weblate
f3b8f6f585
i18n: translation updates from weblate
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Kurt Harren Catindig <kurtharrencatindig03@gmail.com>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-09-01 17:00:54 +00:00
Hojjat
de739425cb fix: rubber band selection not redrawing on mouse movement 2026-08-31 17:21:58 +02:00
35334ba93b chore: fige Cargo.lock tel que resolu apres le merge 1.7.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 07:58:51 +02:00
deb6000c10 Merge tag 'epoch-1.7.0' into local/libcosmic-path-patch
Epoch 1.7.0

# Conflicts:
#	Cargo.lock
#	src/app.rs
2026-08-27 07:52:59 +02:00
Jeremy Soller
67af455898 Epoch 1.7.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-08-25 11:28:07 -06:00
Konstantinos
b46b77f3a7 Align center for text on items in grid view. fixes #754 2026-08-24 17:08:53 -06:00
Jeremy Soller
9642fef9cd
detect remote filesystems so SFTP browsing is not slow (#1996)
- [x] I have disclosed use of any AI generated code in my commit
messages. (didnt use any)
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Fixes #582 

filesystem::remote only comes from query_filesystem_info(), but both
scan
paths read it from enumerate_children(), where it is always missing.
Every
remote file was treated as local, so each guard that skips expensive
work on
remote filesystems did nothing.

Also request only the attributes actually used instead of "*", add
is_dir to
ItemMetadata::GvfsPath, and count children in the background for visible
items.
2026-08-24 15:33:05 -06:00
Paul K
0afac77e83
Merge branch 'master' into fix-remote-browsing-speed 2026-08-24 22:44:41 +02:00
Michael Aaron Murphy
e84d3be4fd
i18n: update translations from weblate 2026-08-24 20:17:19 +02:00
David Carvalho
bb8b61f48b
i18n(pt-BR): update translations from Weblate
Currently translated at 100.0% (244 of 244 strings)

Translation: Pop OS/COSMIC Files
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
2026-08-23 11:38:21 +02:00
Jeremy Soller
7cebbef82f
feat: allow renaming sidebar favorites to custom names (#1911)
Closes #1474 



https://github.com/user-attachments/assets/69fb51ca-225c-43c9-91c9-8f48375e39c4

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-21 07:20:21 -06:00
0f6f9509ae chore: fige Cargo.lock tel que resolu avec les dependances locales
Resolution obtenue avec les patchs de chemin du workspace (libcosmic-yoda,
cosmic-protocols, window_clipboard, cosmic-text). Pas de changement de code,
uniquement le verrou remis en coherence avec l'arbre local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 09:54:41 +02:00
César Mendonça
08a637767d refactor: rename favorite renaming action to "Change sidebar label"
Address review feedback on the sidebar favorite renaming feature:

- Context menu item and dialog title now share the same
  "change-sidebar-label" translation key
- Rename NavMenuAction::RenameFavorite and DialogPage::RenameFavorite
  to ChangeSidebarLabel, with the dialog field renamed to "label"
- Rename Favorite::with_name to Favorite::with_label
- Dialog confirm button now uses the existing "save" key instead of
  "rename-confirm"
- Replace the "rename-favorite"/"favorite-name" keys with
  "sidebar-label" in en and pt-BR translations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRJDQDBKhCqipbcZm7U1v9
2026-08-20 23:16:06 -03:00
César Mendonça
3d520858f3 feat: allow renaming sidebar favorites to custom names
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 23:16:06 -03:00
Jeremy Soller
28546795b0
fix: handle file mtime overflow (#1959)
Modification time is doing an unchecked add. Files with bad mtime will
overflow and crash the app.

Use a checked add instead, returning None and displaying an empty mtime
instead.

This only affects mtimes via GVFS. Viewing local files works because it
interprets the mtime directly instead of relying on addition.

Fixes #1886 

- [x] I have disclosed use of any AI generated code in my commit
messages.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-20 17:21:59 -06:00
Paul K
cca10d2fd3
detect remote filesystems so SFTP browsing is not slow 2026-08-20 17:11:10 +02:00
Will Sheehan
eebf5a9d4c do not panic in truly invalid timestamps 2026-08-19 21:46:45 -07:00
norepro
a5422167cf Return None on mtime overflow 2026-08-19 20:30:31 -07:00
Jeremy Soller
3aa55b07b7
add search field in 'open with' dialog (#1943)
The "Open With" context menu item, which allows you to open a file, now
includes a search option by application name.

Demo:


https://github.com/user-attachments/assets/b4ff5da3-4bdf-4a0f-a20b-b6783f293fef



- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-19 16:01:52 -06:00
1488e2b01b Merge remote-tracking branch 'upstream/master' into local/libcosmic-path-patch
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/app.rs
#	src/dialog.rs
#	src/menu.rs
#	src/tab.rs
2026-08-19 22:02:40 +02:00
sanua356
fd6d4523cd add submit form on focus search input, change import search_input 2026-08-19 09:35:15 +03:00
Hosted Weblate
9bdba47514
i18n: translation updates from weblate
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Michael Millet <mikevlet@protonmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Sproston <ojspr2007@gmail.com>
Co-authored-by: Steen Rabol <steen@rabol.dev>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: sicKat <giuseppecatillo2003@gmail.com>
Co-authored-by: the-forest-watches <lucastronks@protonmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/nl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translation: Pop OS/COSMIC Files
2026-08-19 04:51:30 +00:00
chris-010
3c9a78ac9f fix: accept a URI typed into the location bar from any location
The location bar inherits its type from the tab's current location
(Message::EditLocationEnable), and the view has exactly two branches, next
to the existing "TODO: allow editing other locations": a Location::Network
is edited through with_uri, anything else through with_path. Typing
smb://server/share while standing in a local folder therefore produced
Path("smb://server/share"), which is not a directory, and the navigation
was dropped. The reverse direction was broken the same way: from the
Networks view a local path was passed to with_uri and became a bogus
network location.

EditLocationSubmit now decides by parsing the text instead of by the
location it came from:

  * parses as a URL with a scheme other than "file" and has a host
    -> Location::Network, using url::Url::as_str(), which also normalizes
       away surrounding whitespace.
  * fails to parse and the current location is a Network
    -> Location::Path, with the text untrimmed, because a directory name
       may legitimately end in a space.
  * anything else is left as it was, so network:/// and foo:bar keep
    behaving exactly as before.

The reliable indicator for "this is a path" is that parsing fails, not
that the host is missing: network:/// and file:///home both parse with
has_host() == false. Verified against url 2.5.8.

An unmounted share cannot be resolved, since Tab::resolve sends network
locations through mounter.dir_info, so resolve() returns None and the
navigation would be dropped again. Navigating there is what triggers the
mount, because Cmd::NetworkScan mounts the location it is asked to scan,
so the typed URI is used as a fallback. That fallback is restricted to
input recognised by the first rule above; applying it to any network
location would let unparseable text through, and because with_uri keeps
the display name and path of the previous location, the window would look
normal while the listing stayed empty.

Known cosmetic side effect: while the share is not mounted, resolve()
yields nothing and the tab shows the raw URI as its name. It is replaced
by the proper name as soon as the mount completes.

Drafted with AI assistance (Claude Code); the change was reviewed, built
and tested by me on real hardware as described.

Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com>
2026-08-18 16:01:32 -06:00
Jeremy Soller
e96b915f63
Epoch 1.6.0 version update (#1985)
Generated by cosmic-epoch scripts/version-update.sh
2026-08-18 15:27:42 -06:00
Jeremy Soller
9ff853c46d Epoch 1.6.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-08-18 13:58:04 -06:00
Jeremy Soller
6e210a3753
Add context menu to text fields and Selectable Text widget (#1815)
- Adds context menu to text input fields
- Adds selectable text to context drawer, but only to file name,
created/modified/accessed date. I'm sure more places could use the
selectable text.
- Also pulls the latest changes in Iced and libcosmic that removes
"redraw on any message", which means moving the cursor doesn't
automatically call a redraw, widgets need to ask for it. I have found
all places that previously was working normally because we were
redrawing almost unconditionally. But it is possible that a custom
widget somewhere still isn't asking for a redraw explicitly when its
state changes. So, for QA I'd keep an eye on that.

Depends on:
- [x] https://github.com/pop-os/libcosmic/pull/1288
- [x] https://github.com/pop-os/iced/pull/378
- [x] https://github.com/pop-os/libcosmic/pull/1384
- [x] https://github.com/pop-os/libcosmic/pull/1388


https://github.com/user-attachments/assets/caf8f940-0793-4665-844a-b6153b35d7bb



___
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-18 07:22:09 -06:00
Michael Murphy
9c841dbacd
i18n: translation update from Hosted Weblate (#1948) 2026-08-18 14:10:18 +02:00
Hosted Weblate
991e5b234e
i18n: translation updates from weblate
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Michael Millet <mikevlet@protonmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Sproston <ojspr2007@gmail.com>
Co-authored-by: Steen Rabol <steen@rabol.dev>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: sicKat <giuseppecatillo2003@gmail.com>
Co-authored-by: the-forest-watches <lucastronks@protonmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/nl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translation: Pop OS/COSMIC Files
2026-08-18 05:51:50 +00:00
Hojjat
78ac96c613 chore: update libcosmic 2026-08-17 11:17:19 -06:00
Hojjat
55c1b0afe0 feat: add context menu to text input and selectable text fields
chore: update libcosmic
2026-08-17 11:17:19 -06:00
sanua356
e3aa2a0ec5 fix incorrect search 2026-08-16 11:14:10 +03:00
sanua356
6273fe6505 add autofocus, move label to placeholder, add search and clear icons 2026-08-16 10:30:57 +03:00
Jeremy Soller
5bdfffa494
fix: infinite dialogs with network mount failure (#1956)
Selecting a broken network mount triggers an infinite loop of dialogs
that requires killing the app:

1. Click invalid network mount
2. Try to mount network path
3. Show failure dialog
4. Trigger `NetworkDriveOpenEntityAfterMount`
* BUG: This fires even if the mount failed or the user canceled the
dialog!
6. Goto 2

Fix by having `network_drive(...)` return a Boolean indicating whether
the mount actually happened and using that to break the infinite error
loop.

Fixes #1629 

Sample broken network path that triggers the infinite dialog when
selected:

**~/.config/cosmic/com.system76.CosmicFiles/v1/favorites**

```ron
[
    Home,
    Documents,
    Downloads,
    Network(
        uri: "sftp://myserver/shared",
        name: "",
        path: "/tmp/fake-network/sftp-host=myserver/mnt",
    ),
]
```

- [x] I have disclosed use of any AI generated code in my commit
messages.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-08-11 07:46:35 -06:00
Will Sheehan
95035166d3 fix infinite dialog on network mount failure 2026-08-04 23:09:19 -07:00
Jeremy Soller
24e34eaa0f
Epoch 1.5.0 version update (#1946)
Generated by cosmic-epoch scripts/version-update.sh
2026-07-28 18:07:38 -06:00
Jeremy Soller
0875cda769 Epoch 1.5.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-28 09:14:14 -06:00
Michael Murphy
ebce599f3a
i18n: translation updates from weblate (#1891)
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files

Co-authored-by: Ahrakiel <christopherbatrier@gmail.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: abdelbasset jabrane <naminio201@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: rbrule <raphaelbrule22@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
2026-07-28 16:41:17 +02:00
Hosted Weblate
d6f6179e67
i18n: translation updates from weblate
Co-authored-by: Ahrakiel <christopherbatrier@gmail.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: abdelbasset jabrane <naminio201@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: rbrule <raphaelbrule22@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-files/zh_Hant/
Translation: Pop OS/COSMIC Files
2026-07-28 07:02:19 +02:00
sanua356
9a4f78aa75 remove extra translations 2026-07-26 17:36:56 +03:00
sanua356
a1c1aff60c add search field in 'open with' dialog 2026-07-26 14:46:03 +03:00
Jeremy Soller
9a21542825
fix: not all files directory are trash (#1933)
At the moment any directory anywhere named "files" is considered trash
by cosmic-files and shown as empty.
This tightens the check on what is a trash directory with a fallback
where it also expect an info directory next to the files directory(only
used if the system doesn't report any trash directory).

As an added bonus I fixed the let _ = THUMB_SEMAPHORE.acquire() that was
immediately discarded and thus dead code.

- [X] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [X] I understand these changes in full and will be able to respond to
review comments.
- [X] My change is accurately described in the commit message.
- [X] My contribution is tested and working as described.
- [X] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-07-21 06:55:26 -06:00
Jeremy Soller
7ae2621f70
Epoch 1.4.0 version update (#1938)
Generated by cosmic-epoch scripts/version-update.sh
2026-07-20 18:14:18 -06:00
Jeremy Soller
eb04ae28da Epoch 1.4.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-20 14:36:15 -06:00
RipleyTom
dc36bdc8d3
fix: fixes original_path_for_trash_child 2026-07-19 14:03:04 +02:00
RipleyTom
8ef1082a4c
fix: not all files directory are trash 2026-07-18 19:13:12 +02:00
Joel
a93fc1d9ee
fix: fall back to GIO on destination open failure for MTP copies
* fix: fall back to GIO on destination open failure for MTP copies
* Preserve error source chain in non-gvfs open failure path
2026-07-15 12:42:17 -06:00
Michael Aaron Murphy
5278c67957 fix(libcosmic): crash when right clicking mounted path in nav bar
Contains fix from https://github.com/pop-os/libcosmic/pull/1354
2026-07-14 10:25:57 -06:00
Jeremy Soller
6c45017b24 Epoch 1.3.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-07-13 09:18:38 -06:00
Ashley Wulber
b1fbc1d538 chore: update libcosmic 2026-07-09 17:23:33 -06:00
Ashley Wulber
c4a9096ae2 fix: update iced
popup blur & corner cleanup
2026-07-07 11:36:24 -04:00
Jeremy Soller
fab53e54d6
chore: update libcosmic (#1713)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-07-06 07:11:32 -06:00
Ashley Wulber
168ff6d6f8 chore: updated deps 2026-07-02 14:23:12 -04:00
Ashley Wulber
908218e3e0 fix: remove background style for container around tab bar 2026-07-01 15:18:11 -04:00
Ashley Wulber
8fc5182486 fix: popup corner improvements 2026-07-01 15:18:11 -04:00
Ashley Wulber
e00725a450 chore: update for navbar popup 2026-07-01 15:18:11 -04:00
Ashley Wulber
0e761ba811 fix: no blur for desktop mode
fix: desktop blur

chore: update libcosmic

chore: update libcosmic

chore: update libcosmic
2026-07-01 15:18:11 -04:00
Ashley Wulber
674a33e3de cctk 2026-07-01 15:18:11 -04:00
Ashley Wulber
6fe510a67a chore: update libcosmic 2026-07-01 15:18:09 -04:00
55 changed files with 3292 additions and 1888 deletions

1789
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-files" name = "cosmic-files"
version = "1.3.0" version = "1.9.0"
authors = ["Jeremy Soller <jeremy@system76.com>"] authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
@ -11,16 +11,16 @@ anyhow = "1"
jiff = "0.2" jiff = "0.2"
jiff-icu = "0.2" jiff-icu = "0.2"
icu = { version = "2.2.0", features = ["compiled_data"] } icu = { version = "2.2.0", features = ["compiled_data"] }
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c0cff4d", optional = true }
cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true } cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true }
dirs = "6.0.0" dirs = "6.0.0"
gio = { version = "0.21", optional = true } gio = { version = "0.21", optional = true }
glib = { version = "0.21", optional = true } glib = { version = "0.21", optional = true }
glob = "0.3" glob = "0.3"
ignore = "0.4" ignore = "0.4"
image = "0.25" image.workspace = true
libc = "0.2" libc = "0.2"
log = "0.4" log.workspace = true
mime_guess = "2" mime_guess = "2"
notify-debouncer-full = "0.7" notify-debouncer-full = "0.7"
notify-rust = { version = "4", optional = true } notify-rust = { version = "4", optional = true }
@ -166,4 +166,8 @@ mime = { git = "https://forge.aditua.com/leyoda/window_clipboard.git", branch =
cosmic-text = { path = "../cosmic-text" } cosmic-text = { path = "../cosmic-text" }
[workspace] [workspace]
members = ["cosmic-files-applet"] members = ["cosmic-files-applet", "cosmic-files-thumbnailer"]
[workspace.dependencies]
image = "0.25"
log = "0.4"

View file

@ -1,10 +1,10 @@
[package] [package]
name = "cosmic-files-applet" name = "cosmic-files-applet"
version = "1.2.0" version = "1.9.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
log = "0.4" log.workspace = true
zbus = "4" # Blocking connection in zbus 5 hangs zbus = "4" # Blocking connection in zbus 5 hangs
[dependencies.cosmic-files] [dependencies.cosmic-files]

View file

@ -0,0 +1,8 @@
[package]
name = "cosmic-files-thumbnailer"
version = "1.9.0"
edition = "2024"
[dependencies]
image.workspace = true
pelite = "0.10"

View file

@ -0,0 +1,254 @@
// SPDX-License-Identifier: GPL-3.0-only
use image::{DynamicImage, ImageFormat, RgbaImage};
use std::error::Error;
use std::ffi::{OsStr, OsString};
use std::io;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
thumbnail_from_args(std::env::args_os().skip(1))
}
#[allow(dead_code)]
pub fn thumbnail_from_args(mut args: impl Iterator<Item = OsString>) -> Result<(), Box<dyn Error>> {
let usage = || {
io::Error::new(
io::ErrorKind::InvalidInput,
"usage: cosmic-files-thumbnailer OUTPUT --size SIZE INPUT",
)
};
let output = args.next().ok_or_else(usage)?;
if args.next().as_deref() != Some(OsStr::new("--size")) {
return Err(usage().into());
}
let size = args
.next()
.and_then(|arg| arg.into_string().ok())
.and_then(|arg| arg.parse::<u32>().ok())
.filter(|size| *size > 0)
.ok_or_else(usage)?;
let input = args.next().ok_or_else(usage)?;
if args.next().is_some() {
return Err(usage().into());
}
thumbnail(Path::new(&input), Path::new(&output), size)
}
#[allow(dead_code)]
pub fn thumbnail(input: &Path, output: &Path, size: u32) -> Result<(), Box<dyn Error>> {
let icon = thumbnail_image(input, size)?;
DynamicImage::ImageRgba8(icon).save_with_format(output, ImageFormat::Png)?;
Ok(())
}
pub fn thumbnail_image(input: &Path, size: u32) -> Result<RgbaImage, Box<dyn Error>> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"thumbnail size must be positive",
)
.into());
}
// Map the PE so only the headers and resource pages touched by pelite are read.
let data = pelite::FileMap::open(input)?;
let bytes = data.as_ref();
if bytes.len() < 64 || !bytes.starts_with(b"MZ") {
return Err(io::Error::new(io::ErrorKind::InvalidData, "input is not a PE file").into());
}
let file = pelite::PeFile::from_bytes(bytes).map_err(invalid_data)?;
let resources = file.resources().map_err(invalid_data)?;
let (_name, group) = resources
.icons()
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "PE file has no icon group"))?
.map_err(invalid_data)?;
let mut ico = Vec::new();
group.write(&mut ico).map_err(invalid_data)?;
Ok(
DynamicImage::ImageRgba8(largest_icon(&ico).map_err(invalid_data)?)
.thumbnail(size, size)
.into_rgba8(),
)
}
fn invalid_data(error: impl std::fmt::Display) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, error.to_string())
}
fn largest_icon(ico: &[u8]) -> Result<RgbaImage, String> {
if ico.len() < 6 {
return Err("ICO header is truncated".to_string());
}
let count = usize::from(u16::from_le_bytes([ico[4], ico[5]]));
let mut best: Option<(u64, RgbaImage)> = None;
let mut errors = Vec::new();
for i in 0..count.min(32) {
let entry = 6 + i * 16;
if ico.len() < entry + 16 {
break;
}
let size = u32::from_le_bytes([
ico[entry + 8],
ico[entry + 9],
ico[entry + 10],
ico[entry + 11],
]) as usize;
let offset = u32::from_le_bytes([
ico[entry + 12],
ico[entry + 13],
ico[entry + 14],
ico[entry + 15],
]) as usize;
let Some(end) = offset.checked_add(size) else {
continue;
};
let Some(data) = ico.get(offset..end) else {
continue;
};
match decode_icon(&ico[entry..entry + 16], data) {
Ok(image) => {
let pixels = u64::from(image.width()) * u64::from(image.height());
if best
.as_ref()
.is_none_or(|(best_pixels, _)| pixels > *best_pixels)
{
best = Some((pixels, image));
}
}
Err(err) => errors.push(format!("frame {i}: {err}")),
}
}
best.map(|(_, image)| image).ok_or_else(|| {
if errors.is_empty() {
"PE icon contains no complete frames".to_string()
} else {
format!("PE icon could not be decoded ({})", errors.join("; "))
}
})
}
fn decode_icon(entry: &[u8], data: &[u8]) -> Result<RgbaImage, String> {
if data.starts_with(b"\x89PNG\r\n\x1a\n") {
return image::ImageReader::with_format(io::Cursor::new(data), ImageFormat::Png)
.decode()
.map(DynamicImage::into_rgba8)
.map_err(|err| err.to_string());
}
let capacity = 22usize
.checked_add(data.len())
.ok_or_else(|| "frame is too large".to_string())?;
let data_len = u32::try_from(data.len()).map_err(|_| "frame is too large".to_string())?;
let mut ico = Vec::with_capacity(capacity);
ico.extend_from_slice(&[0, 0, 1, 0, 1, 0]);
ico.extend_from_slice(entry);
ico[14..18].copy_from_slice(&data_len.to_le_bytes());
ico[18..22].copy_from_slice(&22u32.to_le_bytes());
ico.extend_from_slice(data);
image::ImageReader::with_format(io::Cursor::new(ico), ImageFormat::Ico)
.decode()
.map(DynamicImage::into_rgba8)
.map_err(|err| err.to_string())
}
#[cfg(test)]
mod tests {
use super::{largest_icon, thumbnail_from_args};
use image::{DynamicImage, ImageFormat, Rgb, RgbImage, Rgba, RgbaImage};
use std::ffi::OsString;
use std::io::Cursor;
#[test]
fn malformed_thumbnail_request_returns_an_error() {
let args = [OsString::from("output.png")];
assert!(thumbnail_from_args(args.into_iter()).is_err());
}
#[test]
fn largest_icon_skips_invalid_entries() {
let image = RgbaImage::from_pixel(2, 2, Rgba([1, 2, 3, 255]));
let mut png = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(image)
.write_to(&mut png, ImageFormat::Png)
.unwrap();
let png = png.into_inner();
let mut ico = vec![0, 0, 1, 0, 2, 0];
let mut invalid_entry = [0; 16];
invalid_entry[8..12].copy_from_slice(&4u32.to_le_bytes());
invalid_entry[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
ico.extend_from_slice(&invalid_entry);
let mut valid_entry = [0; 16];
valid_entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 32, 0]);
valid_entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
valid_entry[12..16].copy_from_slice(&38u32.to_le_bytes());
ico.extend_from_slice(&valid_entry);
ico.extend_from_slice(&png);
let decoded = largest_icon(&ico).expect("valid icon after malformed entry");
assert_eq!(decoded.dimensions(), (2, 2));
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
}
#[test]
fn dib_and_mask_is_applied() {
let mut dib = vec![0; 40];
dib[0..4].copy_from_slice(&40u32.to_le_bytes());
dib[4..8].copy_from_slice(&2i32.to_le_bytes());
dib[8..12].copy_from_slice(&4i32.to_le_bytes());
dib[12..14].copy_from_slice(&1u16.to_le_bytes());
dib[14..16].copy_from_slice(&24u16.to_le_bytes());
// Two bottom-up BGR rows, padded to four-byte boundaries.
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
dib.extend_from_slice(&[0, 0, 255, 0, 0, 255, 0, 0]);
// The first mask row is the bottom image row; its first pixel is transparent.
dib.extend_from_slice(&[0b1000_0000, 0, 0, 0]);
dib.extend_from_slice(&[0, 0, 0, 0]);
let mut ico = vec![0, 0, 1, 0, 1, 0];
let mut entry = [0; 16];
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
entry[8..12].copy_from_slice(&u32::try_from(dib.len()).unwrap().to_le_bytes());
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
ico.extend_from_slice(&entry);
ico.extend_from_slice(&dib);
let decoded = largest_icon(&ico).expect("valid DIB icon");
assert_eq!(decoded.get_pixel(0, 0), &Rgba([255, 0, 0, 255]));
assert_eq!(decoded.get_pixel(0, 1), &Rgba([255, 0, 0, 0]));
}
#[test]
fn rgb_png_icon_is_decoded() {
let image = RgbImage::from_pixel(2, 2, Rgb([1, 2, 3]));
let mut png = Cursor::new(Vec::new());
DynamicImage::ImageRgb8(image)
.write_to(&mut png, ImageFormat::Png)
.unwrap();
let png = png.into_inner();
let mut ico = vec![0, 0, 1, 0, 1, 0];
let mut entry = [0; 16];
entry[..8].copy_from_slice(&[2, 2, 0, 0, 1, 0, 24, 0]);
entry[8..12].copy_from_slice(&u32::try_from(png.len()).unwrap().to_le_bytes());
entry[12..16].copy_from_slice(&22u32.to_le_bytes());
ico.extend_from_slice(&entry);
ico.extend_from_slice(&png);
let decoded = largest_icon(&ico).expect("valid RGB PNG icon");
assert_eq!(decoded.get_pixel(0, 0), &Rgba([1, 2, 3, 255]));
}
}

42
debian/changelog vendored
View file

@ -1,3 +1,45 @@
cosmic-files (1.9.0) noble; urgency=medium
* Epoch 1.9.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 20:35:19 +0200
cosmic-files (1.8.0) noble; urgency=medium
* Epoch 1.8.0 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:14:53 -0600
cosmic-files (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 25 Aug 2026 09:20:22 -0600
cosmic-files (1.6.0) noble; urgency=medium
* Epoch 1.6.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 18 Aug 2026 07:13:48 -0600
cosmic-files (1.5.0) noble; urgency=medium
* Epoch 1.5.0 version update
-- Jeremy Soller <jeremy@system76.com> Tue, 28 Jul 2026 09:14:09 -0600
cosmic-files (1.4.0) noble; urgency=medium
* Epoch 1.4.0 version update
-- Jeremy Soller <jeremy@system76.com> Mon, 20 Jul 2026 14:36:06 -0600
cosmic-files (1.3.0) noble; urgency=medium
* Epoch 1.3.0 version update
-- Jeremy Soller <jeremy@system76.com> Sun, 12 Jul 2026 13:21:18 -0600
cosmic-files (1.2.0) noble; urgency=medium cosmic-files (1.2.0) noble; urgency=medium
* Epoch 1.2.0 version update * Epoch 1.2.0 version update

View file

@ -1,4 +1,6 @@
tar-ignore=.github tar-ignore=.github
tar-ignore=.vscode tar-ignore=target/.rustc_info.json
tar-ignore=target/CACHEDIR.TAG
tar-ignore=target/debug
tar-ignore=target/release
tar-ignore=vendor tar-ignore=vendor
tar-ignore=target

View file

@ -84,7 +84,7 @@ settings = الإعدادات
### Appearance ### Appearance
appearance = المظهر appearance = المظهر
theme = النسق theme = السمة
match-desktop = طابق سطح المكتب match-desktop = طابق سطح المكتب
dark = داكن dark = داكن
light = فاتح light = فاتح
@ -110,7 +110,7 @@ new-tab = لسان جديد
new-window = نافذة جديدة new-window = نافذة جديدة
rename = غيِّر الاسم... rename = غيِّر الاسم...
close-tab = أغلق اللسان close-tab = أغلق اللسان
quit = أنهِ quit = إنهِ
## Edit ## Edit
@ -147,7 +147,7 @@ icon-size-and-spacing = حجم الأيقونة والتباعد
icon-size = حجم الأيقونة icon-size = حجم الأيقونة
grid-spacing = تباعد شبكي grid-spacing = تباعد شبكي
trashed-on = مهمل trashed-on = مهمل
details = التفاصيل details = تفاصيل
pause = ألبث pause = ألبث
resume = استئناف resume = استئناف
create-archive = أنشئ أرشيف create-archive = أنشئ أرشيف
@ -383,3 +383,7 @@ context-action-confirm-warning =
*[other] عنصر *[other] عنصر
}. }.
run = شغِّل run = شغِّل
checksum = تدقيق المجموع { $kind }
calculate = احسب
error = خطأ
rename-confirm = أعد التسمية

View file

@ -77,7 +77,7 @@ name-no-slashes = El nom no pot contenir barres.
cancel = Cancel·lar cancel = Cancel·lar
create = Crea create = Crea
open = Obre open = Obre
open-file = Obre el fixer open-file = Obre el fitxer
open-folder = Obre la carpeta open-folder = Obre la carpeta
open-in-new-tab = Obre en una pestanya nova open-in-new-tab = Obre en una pestanya nova
open-in-new-window = Obre en una finestra nova open-in-new-window = Obre en una finestra nova

View file

@ -428,3 +428,9 @@ context-action-confirm-warning =
}. }.
run = Spustit run = Spustit
rename-confirm = Přejmenovat rename-confirm = Přejmenovat
checksum = Kontrolní součet { $kind }
calculate = Vypočítat
error = Chyba
search-application = Hledat podle názvu aplikace
sidebar-label = Popisek v postranním panelu
change-sidebar-label = Změnit popisek

View file

@ -1,35 +1,43 @@
cosmic-files = COSMIC Filer cosmic-files = COSMIC Filhåndtering
empty-folder = Tom mappe empty-folder = Tom mappe
empty-folder-hidden = Tom mappe (har skjulte filer) empty-folder-hidden = Tom mappe (har skjulte emner)
no-results = Ingen resultater no-results = Fandt ingen resultater
filesystem = Filsystem filesystem = Filsystem
home = Hjem home = Hjem
networks = Netværk networks = Netværk
notification-in-progress = Filoperationer er igangværende. notification-in-progress = Fil-operationer er igang
trash = Skraldespand trash = Papirkurv
recents = Seneste recents = Seneste
undo = Fortryd undo = Fortryd
today = I dag today = I dag
# Desktop view options # Desktop view options
desktop-view-options = Indstillinger for skrivebordsudseende... desktop-view-options = Valgmuligheder for skrivebordsvisning...
show-on-desktop = Vis på Skrivebordet show-on-desktop = Vis på Skrivebord
desktop-folder-content = Indhold på Skrivebordet desktop-folder-content = Skrivebords-mappeindhold
mounted-drives = Monterede drev mounted-drives = Monterede drev
trash-folder-icon = Skraldespandsikon trash-folder-icon = Papirkurv-ikon
icon-size-and-spacing = Ikonstørrelse og afstand icon-size-and-spacing = Ikonstørrelse og -afstand
icon-size = Ikonstørrelse icon-size = Ikonstørrelse
# List view # List view
name = Navn name = Navn
modified = Ændret modified = Ændret
trashed-on = Smidt ud trashed-on = Kasseret
size = Størrelse size = Størrelse
# Progress footer # Progress footer
details = Detaljer details = Detaljer
dismiss = Afvis besked dismiss = Luk meddelelse
operations-running = { $running } operationer er i gang ({ $percent }%)... operations-running =
operations-running-finished = { $running } operationer er i gang ({ $percent }%), { $finished } færdiggjort... { $running } { $running ->
[one] operation
*[other] operationer
} kører ({ $percent }%)...
operations-running-finished =
{ $running } { $running ->
[one] operation
*[other] operationer
} kører ({ $percent }%), { $finished } færdig...
pause = Pause pause = Pause
resume = Fortsæt resume = Genoptag
# Dialogs # Dialogs
@ -40,12 +48,12 @@ create-archive = Opret arkiv
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = Tøm skraldespand empty-trash = Tøm papirkurv
empty-trash-warning = Er du sikker på du vil slette alle objekter i Skraldespanden permanent? empty-trash-warning = Emner i Papirkurv-mappen vil blive slettet permanent
## Mount Error Dialog ## Mount Error Dialog
mount-error = Kan ikke tilgå drev mount-error = Ude af stand til at tilgå drev
## New File/Folder Dialog ## New File/Folder Dialog
@ -53,22 +61,22 @@ create-new-file = Opret ny fil
create-new-folder = Opret ny mappe create-new-folder = Opret ny mappe
file-name = Filnavn file-name = Filnavn
folder-name = Mappenavn folder-name = Mappenavn
file-already-exists = En fil med det navn eksisterer allerede. file-already-exists = En fil med det navn eksisterer allerede
folder-already-exists = En mappe med det navn eksisterer allerede. folder-already-exists = En mappe med det navn eksisterer allerede
name-hidden = Navne begyndende med "." vil blive skjult. name-hidden = Navne begyndende med "." vil være skjulte
name-invalid = Navn kan ikke være "{ $filename }". name-invalid = Navn må ikke være "{ $filename }"
name-no-slashes = Navn kan ikke indeholde skråstreg. name-no-slashes = Navn må ikke indeholde skråstreger
## Open/Save Dialog ## Open/Save Dialog
cancel = Annullér cancel = Afbryd
create = Opret create = Opret
open = Åbn open = Åbn
open-file = Åbn fil open-file = Åbn fil
open-folder = Åbn mappe open-folder = Åbn mappe
open-in-new-tab = Åbn i ny fane open-in-new-tab = Åbn i nyt faneblad
open-in-new-window = Åbn i nyt vindue open-in-new-window = Åbn i nyt vindue
open-item-location = Åbn placering for objekt open-item-location = Åbn emne-placering
open-multiple-files = Åbn flere filer open-multiple-files = Åbn flere filer
open-multiple-folders = Åbn flere mapper open-multiple-folders = Åbn flere mapper
save = Gem save = Gem
@ -87,20 +95,20 @@ rename-folder = Omdøb mappe
## Replace Dialog ## Replace Dialog
replace = Erstat replace = Erstat
replace-title = "{ $filename }" eksisterer allerede på denne placering. replace-title = "{ $filename }" eksisterer allerede i denne placering
replace-warning = Vil du erstatte den med den du er ved at gemme? Hvis du erstatter den, overskriver du dens indhold. replace-warning = Ønsker du at erstatte den med den, du er ved at gemme? Erstatning af den vil overskrive dens indhold.
replace-warning-operation = Vil du erstatte den? Hvis du erstatter den, overskriver du dens indhold. replace-warning-operation = Ønsker du at erstatte den? Erstatning af den vil overskrive dens indhold.
original-file = Original fil original-file = Original fil
replace-with = Erstat med replace-with = Erstat med
apply-to-all = Anvend for alle apply-to-all = Anvend på alle
keep-both = Behold begge keep-both = Behold begge
skip = Spring over skip = Spring over
## Set as Executable and Launch Dialog ## Set as Executable and Launch Dialog
set-executable-and-launch = Indstil som eksekverbar fil og start set-executable-and-launch = Angiv som eksekverbar og start
set-executable-and-launch-description = Vil du indstille "{ $name }" som en eksekverbar fil og starte den? set-executable-and-launch-description = Vil du angive "{ $name }" som eksekverbar og starte den?
set-and-launch = Indstil og start set-and-launch = Angiv og start
## Metadata Dialog ## Metadata Dialog
@ -239,13 +247,13 @@ settings = Indstillinger
appearance = Udseende appearance = Udseende
theme = Tema theme = Tema
match-desktop = Match skrivebord match-desktop = Følg skrivebordets indstillinger
dark = Mørk dark = Mørkt
light = Lys light = Lyst
# Context menu # Context menu
add-to-sidebar = Tilføj til sidebjælke add-to-sidebar = Tilføj til sidebjælke
compress = Komprimér compress = Komprimer…
extract-here = Extract extract-here = Uddrag
new-file = Ny fil... new-file = Ny fil...
new-folder = Ny mappe... new-folder = Ny mappe...
open-in-terminal = Åbn i terminal open-in-terminal = Åbn i terminal
@ -280,15 +288,15 @@ quit = Afslut
edit = Redigér edit = Redigér
cut = Klip cut = Klip
copy = Kopiér copy = Kopiér
paste = Sæt ind paste = Indsæt
select-all = Vælg alt select-all = Vælg alle
## View ## View
zoom-in = Zoom ind zoom-in = Zoom ind
default-size = Standardstørrelse default-size = Standardstørrelse
zoom-out = Zoom ud zoom-out = Zoom ud
view = Visning view = Vis
grid-view = Gitter-visning grid-view = Gitter-visning
list-view = Liste-visning list-view = Liste-visning
show-hidden-files = Vis skjulte filer show-hidden-files = Vis skjulte filer
@ -306,3 +314,102 @@ sort-newest-first = Nyeste først
sort-oldest-first = Ældste først sort-oldest-first = Ældste først
sort-smallest-to-largest = Mindste til største sort-smallest-to-largest = Mindste til største
sort-largest-to-smallest = Største til mindste sort-largest-to-smallest = Største til mindste
delete = Slet
run = Kør
repository = Pakkearkiv
grid-spacing = Gitter-afstand
extract-password-required = Adgangskode påkrævet
extract-to = Udpak Til...
extract-to-title = Udpak til mappe
empty-trash-title = Tøm papirkurv?
keywords = Mappe;Håndtering;
comment = Filhåndteringsprogram til COSMIC-skrivebordet
copy-to-title = Vælg kopi destination
copy-to-button-label = Kopier
move-to-title = Vælg flytte destination
move-to-button-label = Flyt
deleted =
Slettede { $items } { $items ->
[one] element
*[other] elementer
} fra { trash }
removing-from-recents =
Fjerner { $items } { $items ->
[one] element
*[other] elementer
} fra { recents }
type-to-search-enter-path = Indtast stien til mappen eller filen
permanently-deleting =
Sletter permanent { $items } { $items ->
[one] element
*[other] elementer
}
execute-only = Kun kørsel
removed-from-recents =
Fjernede { $items } { $items ->
[one] element
*[other] elementer
} fra { recents }
permanently-deleted =
Slettede permanent { $items } { $items ->
[one] element
*[other] elementer
}
write-only = Kun skrivning
remove = Fjern
context-action = Konteksthandling
context-action-confirm-title = Kør “{ $name }”?
context-action-confirm-warning =
Dette vil køre på { $items } { $items ->
[one] item
*[other] items
}.
selected-items = De { $items } valgte elementer
mixed = Blandet
none = Ingen
write-execute = Skriv og kør
read-only = Kun læsning
read-execute = Læs og kør
read-write = Læs og skriv
read-write-execute = Læs, skriv og kør
keep = Behold
support = Support
progress-failed = { $percent } %, mislykkedes
pasted-image = Indsat billede
pasted-text = Indsat tekst
pasted-video = Indsat video
deleting =
Sletter { $items } { $items ->
[one] element
*[other] elementer
} fra { trash } ({ $progress })...
setting-permissions = Indstiller tilladelser for “{ $name }” til { $mode }
set-permissions = Sæt tilladelser for “{ $name }” til { $mode }
show-recents = Seneste-mappe i sidepanelet
type-to-search = Skriv for at søge
type-to-search-recursive = Søger i den aktuelle mappe og alle undermapper
type-to-search-select = Vælger den første matchende fil eller mappe
clear-recents-history = Ryd seneste historik
copy-to = Kopiér til…
delete-permanently = Slet permanent
eject = Skub ud
move-to = Flyt til…
remove-from-recents = Fjern fra seneste
reload-folder = Genindlæs mappe
copy-path = Kopiér sti
rename-confirm = Omdøb
calculate = Udregn
error = Fejl
checksum = { $kind }-kontrolsum
other-apps = Andre applikationer
related-apps = Relaterede applikationer
permanently-delete-question = Slet permanent?
permanently-delete-warning = { $target } vil blive slettet permanent. Denne handling kan ikke fortrydes.
open-with = Åbn med
favorite-path-error = Fejl ved åbning af sti
favorite-path-error-description =
Ude af stand til at åbne "{ $path }"
"{ $path }" Eksisterer muligvis ikke, eller du har måske ikketilladelse til at åbne den
Ønsker du at fjerne den fra sidebjælken?
single-click = Enkelt klik for at åbne

View file

@ -11,7 +11,7 @@ recents = Zuletzt verwendet
undo = Rückgängig undo = Rückgängig
today = Heute today = Heute
# Optionen für die Desktop-Ansicht # Optionen für die Desktop-Ansicht
desktop-view-options = Optionen für die Desktop-Ansicht… desktop-view-options = Optionen für die Desktop-Ansicht ...
show-on-desktop = Auf Desktop anzeigen show-on-desktop = Auf Desktop anzeigen
desktop-folder-content = Inhalt des Desktop-Ordners desktop-folder-content = Inhalt des Desktop-Ordners
mounted-drives = Eingehängte Laufwerke mounted-drives = Eingehängte Laufwerke
@ -31,12 +31,12 @@ operations-running =
{ $running } { $running -> { $running } { $running ->
[one] laufender Vorgang [one] laufender Vorgang
*[other] laufende Vorgänge *[other] laufende Vorgänge
} ({ $percent } %)... } ({ $percent } %) ...
operations-running-finished = operations-running-finished =
{ $running } { $running -> { $running } { $running ->
[one] laufender Vorgang [one] laufender Vorgang
*[other] laufende Vorgänge *[other] laufende Vorgänge
} ({ $percent } %), { $finished } abgeschlossen... } ({ $percent } %), { $finished } abgeschlossen ...
pause = Pause pause = Pause
resume = Fortsetzen resume = Fortsetzen
@ -50,7 +50,7 @@ create-archive = Archiv erstellen
## Entpacken-Dialog ## Entpacken-Dialog
extract-password-required = Passwort erforderlich extract-password-required = Passwort erforderlich
extract-to = Entpacken nach... extract-to = Entpacken nach ...
extract-to-title = In Ordner entpacken extract-to-title = In Ordner entpacken
## Dialog zum Leeren des Papierkorbs ## Dialog zum Leeren des Papierkorbs
@ -179,7 +179,7 @@ keep = Behalten
add-network-drive = Netzlaufwerk hinzufügen add-network-drive = Netzlaufwerk hinzufügen
connect = Verbinden connect = Verbinden
connect-anonymously = Anonym verbinden connect-anonymously = Anonym verbinden
connecting = Wird verbunden... connecting = Wird verbunden ...
domain = Domain domain = Domain
enter-server-address = Serveradresse eingeben enter-server-address = Serveradresse eingeben
network-drive-description = network-drive-description =
@ -218,7 +218,7 @@ compressing =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} von „{ $from }“ nach „{ $to }“ komprimiert ({ $progress })... } von „{ $from }“ nach „{ $to }“ komprimiert ({ $progress }) ...
compressed = compressed =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
@ -231,7 +231,7 @@ copying =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} von „{ $from }“ nach „{ $to }“ kopiert ({ $progress })... } von „{ $from }“ nach „{ $to }“ kopiert ({ $progress }) ...
copied = copied =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
@ -241,19 +241,19 @@ deleting =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} aus dem { trash } gelöscht ({ $progress })... } aus dem { trash } gelöscht ({ $progress }) ...
deleted = deleted =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
*[other] Elemente wurden *[other] Elemente wurden
} aus dem { trash } gelöscht } aus dem { trash } gelöscht
emptying-trash = { trash } wird geleert ({ $progress })... emptying-trash = { trash } wird geleert ({ $progress }) ...
emptied-trash = { trash } geleert emptied-trash = { trash } geleert
extracting = extracting =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} von „{ $from }“ nach „{ $to }“ entpackt ({ $progress })... } von „{ $from }“ nach „{ $to }“ entpackt ({ $progress }) ...
extracted = extracted =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
@ -267,7 +267,7 @@ moving =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} von „{ $from }“ nach „{ $to }“ verschoben ({ $progress })... } von „{ $from }“ nach „{ $to }“ verschoben ({ $progress }) ...
moved = moved =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
@ -289,7 +289,7 @@ restoring =
{ $items } { $items -> { $items } { $items ->
[one] Element wird [one] Element wird
*[other] Elemente werden *[other] Elemente werden
} aus dem { trash } wiederhergestellt ({ $progress })... } aus dem { trash } wiederhergestellt ({ $progress }) ...
restored = restored =
{ $items } { $items -> { $items } { $items ->
[one] Element wurde [one] Element wurde
@ -299,7 +299,7 @@ unknown-folder = unbekannter Ordner
## Öffnen mit ## Öffnen mit
menu-open-with = Öffnen mit... menu-open-with = Öffnen mit ...
default-app = { $name } (Standard) default-app = { $name } (Standard)
## Details anzeigen ## Details anzeigen
@ -311,7 +311,7 @@ item-size = Größe: { $size }
item-created = Erstellt: { $created } item-created = Erstellt: { $created }
item-modified = Geändert: { $modified } item-modified = Geändert: { $modified }
item-accessed = Zugegriffen: { $accessed } item-accessed = Zugegriffen: { $accessed }
calculating = Wird berechnet... calculating = Wird berechnet ...
## Einstellungen ## Einstellungen
@ -333,15 +333,15 @@ type-to-search-recursive = Durchsucht den aktuellen Ordner und alle Unterordner
type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein type-to-search-enter-path = Gib den Pfad zum Verzeichnis oder zur Datei ein
# Kontextmenü # Kontextmenü
add-to-sidebar = Zur Seitenleiste hinzufügen add-to-sidebar = Zur Seitenleiste hinzufügen
compress = Komprimieren... compress = Komprimieren ...
delete-permanently = Endgültig löschen delete-permanently = Endgültig löschen
extract-here = Entpacken extract-here = Entpacken
new-file = Neue Datei... new-file = Neue Datei ...
new-folder = Neuer Ordner... new-folder = Neuer Ordner ...
open-in-terminal = Im Terminal öffnen open-in-terminal = Im Terminal öffnen
move-to-trash = In den Papierkorb verschieben move-to-trash = In den Papierkorb verschieben
restore-from-trash = Aus dem Papierkorb wiederherstellen restore-from-trash = Aus dem Papierkorb wiederherstellen
remove-from-sidebar = Von der Seitenleiste entfernen remove-from-sidebar = Aus der Seitenleiste entfernen
sort-by-name = Nach Name sortieren sort-by-name = Nach Name sortieren
sort-by-modified = Nach Änderung sortieren sort-by-modified = Nach Änderung sortieren
sort-by-size = Nach Größe sortieren sort-by-size = Nach Größe sortieren
@ -349,9 +349,9 @@ sort-by-trashed = Nach Löschzeitpunkt sortieren
## Desktop ## Desktop
change-wallpaper = Hintergrundbild ändern... change-wallpaper = Hintergrundbild ändern ...
desktop-appearance = Desktop-Aussehen... desktop-appearance = Desktop-Aussehen ...
display-settings = Anzeigeeinstellungen... display-settings = Anzeigeeinstellungen ...
# Menü # Menü
@ -362,7 +362,7 @@ file = Datei
new-tab = Neuer Tab new-tab = Neuer Tab
new-window = Neues Fenster new-window = Neues Fenster
reload-folder = Ordner neu laden reload-folder = Ordner neu laden
rename = Umbenennen... rename = Umbenennen ...
close-tab = Tab schließen close-tab = Tab schließen
quit = Beenden quit = Beenden
@ -385,8 +385,8 @@ list-view = Listenansicht
show-hidden-files = Versteckte Dateien anzeigen show-hidden-files = Versteckte Dateien anzeigen
list-directories-first = Verzeichnisse zuerst auflisten list-directories-first = Verzeichnisse zuerst auflisten
gallery-preview = Galerie-Vorschau gallery-preview = Galerie-Vorschau
menu-settings = Einstellungen... menu-settings = Einstellungen ...
menu-about = Über COSMIC Dateien... menu-about = Über COSMIC Dateien ...
## Sortieren ## Sortieren
@ -397,7 +397,7 @@ sort-newest-first = Neueste zuerst
sort-oldest-first = Älteste zuerst sort-oldest-first = Älteste zuerst
sort-smallest-to-largest = Von klein nach groß sort-smallest-to-largest = Von klein nach groß
sort-largest-to-smallest = Von groß nach klein sort-largest-to-smallest = Von groß nach klein
repository = Repository repository = Repositorium
empty-trash-title = Papierkorb leeren? empty-trash-title = Papierkorb leeren?
copy-to-title = Kopierziel auswählen copy-to-title = Kopierziel auswählen
copy-to-button-label = Kopieren copy-to-button-label = Kopieren
@ -418,7 +418,7 @@ type-to-search-select = Wählt die erste übereinstimmende Datei oder den ersten
pasted-image = Eingefügtes Bild pasted-image = Eingefügtes Bild
pasted-text = Eingefügter Text pasted-text = Eingefügter Text
pasted-video = Eingefügtes Video pasted-video = Eingefügtes Video
copy-to = Kopieren nach... copy-to = Kopieren nach ...
show-recents = Ordner zuletzt verwendeter Elemente in der Seitenleiste show-recents = Ordner zuletzt verwendeter Elemente in der Seitenleiste
clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren clear-recents-history = Verlauf zuletzt verwendeter Elemente leeren
comment = Dateimanager für den COSMIC Desktop comment = Dateimanager für den COSMIC Desktop
@ -426,7 +426,7 @@ keywords = Ordner;Manager;
move-to-button-label = Verschieben move-to-button-label = Verschieben
move-to-title = Verschiebeziel auswählen move-to-title = Verschiebeziel auswählen
remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen remove-from-recents = Aus den zuletzt verwendeten Elementen entfernen
move-to = Verschieben nach... move-to = Verschieben nach ...
copy-path = Pfad kopieren copy-path = Pfad kopieren
mixed = Gemischt mixed = Gemischt
context-action = Kontextaktion context-action = Kontextaktion
@ -438,3 +438,8 @@ context-action-confirm-warning =
}. }.
run = Ausführen run = Ausführen
rename-confirm = Umbenennen rename-confirm = Umbenennen
checksum = { $kind } Prüfsumme
calculate = Berechnen
error = Fehler
sidebar-label = Bezeichnung in der Seitenleiste
search-application = Nach App-Name suchen

View file

@ -22,7 +22,7 @@ password = Κωδικός πρόσβασης
remove = Αφαίρεση remove = Αφαίρεση
create = Δημιουργία create = Δημιουργία
pause = Παύση pause = Παύση
quit = Έξοδος quit = Τερματισμός
calculating = Υπολογισμός... calculating = Υπολογισμός...
keep = Διατήρηση keep = Διατήρηση
edit = Επεξεργασία edit = Επεξεργασία
@ -328,3 +328,9 @@ context-action-confirm-warning =
*[other] στοιχεία *[other] στοιχεία
}. }.
rename-confirm = Μετονομασία rename-confirm = Μετονομασία
checksum = Άθροισμα ελέγχου { $kind }
calculate = Υπολογισμός
error = Σφάλμα
sidebar-label = Ετικέτα πλαϊνής στήλης
search-application = Αναζήτηση με όνομα εφαρμογής
change-sidebar-label = Αλλαγή ετικέτας πλαϊνής στήλης

View file

@ -0,0 +1,100 @@
save = Save
quit = Quit
name = Name
delete = Delete
cancel = Cancel
run = Run
new-window = New window
theme = Theme
appearance = Appearance
light = Light
repository = Repository
support = Support
dark = Dark
match-desktop = Match desktop
settings = Settings
replace = Replace
file = File
zoom-in = Zoom in
select-all = Select all
zoom-out = Zoom out
default-size = Default size
edit = Edit
copy = Copy
paste = Paste
view = View
undo = Undo
cut = Cut
icon-size-and-spacing = Icon size and spacing
cosmic-files = COSMIC Files
trash = Rubbish
empty-folder-hidden = Empty folder (has hidden items)
no-results = No results found
empty-folder = Empty folder
menu-settings = Settings...
recents = Recents
mounted-drives = Mounted drives
filesystem = Filesystem
home = Home
desktop-view-options = Desktop view options...
show-on-desktop = Show on Desktop
trash-folder-icon = Bin folder icon
desktop-folder-content = Desktop folder content
networks = Networks
today = Today
notification-in-progress = File operations are in progress
comment = File manager for the COSMIC desktop
keywords = Folder;Manager;
size = Size
dismiss = Dismiss message
trashed-on = Binned
create-archive = Create archive
pause = Pause
resume = Resume
grid-spacing = Grid spacing
details = Details
operations-running-finished =
{ $running } { $running ->
[one] operation
*[other] operations
} running ({ $percent }%), { $finished } finished...
icon-size = Icon size
modified = Modified
operations-running =
{ $running } { $running ->
[one] operation
*[other] operations
} running ({ $percent }%)...
copy-to-title = Select copy destination
open-file = Open file
open-multiple-files = Open multiple files
create-new-folder = Create new folder
create = Create
other-apps = Other applications
open-multiple-folders = Open multiple folders
extract-to = Extract To...
create-new-file = Create new file
open-in-new-window = Open in new window
folder-name = Folder name
browse-store = Browse { $store }
open-item-location = Open item location
mount-error = Unable to access drive
open = Open
extract-to-title = Extract to folder
open-with-title = How do you want to open "{ $name }"?
extract-password-required = Password required
open-folder = Open folder
open-in-new-tab = Open in new tab
file-name = File name
save-file = Save file
file-already-exists = A file with that name already exists
name-hidden = Names starting with "." will be hidden
folder-already-exists = A folder with that name already exists
empty-trash = Empty bin
empty-trash-title = Empty bin?
name-no-slashes = Name cannot contain slashes
name-invalid = Name cannot be "{ $filename }"
copy-to-button-label = Copy
move-to-title = Select move destination
move-to-button-label = Move
context-action = Context action

View file

@ -119,6 +119,9 @@ rename-file = Rename file
rename-folder = Rename folder rename-folder = Rename folder
rename-confirm = Rename rename-confirm = Rename
## Change Sidebar Label Dialog
sidebar-label = Sidebar label
## Replace Dialog ## Replace Dialog
replace = Replace replace = Replace
replace-title = "{$filename}" already exists in this location replace-title = "{$filename}" already exists in this location
@ -299,6 +302,7 @@ unknown-folder = unknown folder
## Open with ## Open with
menu-open-with = Open with... menu-open-with = Open with...
default-app = {$name} (default) default-app = {$name} (default)
search-application = Search by app name
## Show details ## Show details
show-details = Show details show-details = Show details
@ -347,6 +351,7 @@ move-to = Move to...
move-to-trash = Move to trash move-to-trash = Move to trash
restore-from-trash = Restore from trash restore-from-trash = Restore from trash
remove-from-sidebar = Remove from sidebar remove-from-sidebar = Remove from sidebar
change-sidebar-label = Change sidebar label
sort-by-name = Sort by name sort-by-name = Sort by name
sort-by-modified = Sort by modified sort-by-modified = Sort by modified
sort-by-size = Sort by size sort-by-size = Sort by size

View file

@ -345,3 +345,4 @@ sort-newest-first = جدیدترین در ابتدا
sort-oldest-first = قدیمی‌ترین در ابتدا sort-oldest-first = قدیمی‌ترین در ابتدا
sort-smallest-to-largest = کوچک‌ترین به بزرگ‌ترین sort-smallest-to-largest = کوچک‌ترین به بزرگ‌ترین
sort-largest-to-smallest = بزرگ‌ترین به کوچک‌ترین sort-largest-to-smallest = بزرگ‌ترین به کوچک‌ترین
run = اجرا

View file

@ -225,7 +225,7 @@ settings = Asetukset
appearance = Ulkoasu appearance = Ulkoasu
theme = Teema theme = Teema
match-desktop = Sovita työpöytään match-desktop = Sama kuin työpöytä
dark = Tumma dark = Tumma
light = Vaalea light = Vaalea
@ -415,3 +415,9 @@ context-action-confirm-warning =
}. }.
run = Suorita run = Suorita
rename-confirm = Nimeä uudelleen rename-confirm = Nimeä uudelleen
checksum = { $kind }-tarkistussumma
calculate = Laske
error = Virhe
search-application = Hae sovelluksen nimellä
sidebar-label = Sivupalkin nimike
change-sidebar-label = Muuta sivupalkin nimikettä

View file

View file

@ -80,14 +80,14 @@ cancel = Annuler
create = Créer create = Créer
open = Ouvrir open = Ouvrir
open-file = Ouvrir le fichier open-file = Ouvrir le fichier
open-folder = Ouvrir dossier open-folder = Ouvrir le dossier
open-in-new-tab = Ouvrir dans un nouvel onglet open-in-new-tab = Ouvrir dans un nouvel onglet
open-in-new-window = Ouvrir dans une nouvelle fenêtre open-in-new-window = Ouvrir dans une nouvelle fenêtre
open-item-location = Ouvrir l'emplacement de l'élément open-item-location = Ouvrir l'emplacement de l'élément
open-multiple-files = Ouvrir plusieurs fichiers open-multiple-files = Ouvrir plusieurs fichiers
open-multiple-folders = Ouvrir plusieurs dossiers open-multiple-folders = Ouvrir plusieurs dossiers
save = Enregistrer save = Enregistrer
save-file = Enregistrer fichier save-file = Enregistrer le fichier
## Open With Dialog ## Open With Dialog
@ -189,7 +189,7 @@ keep = Conserver
## Add Network Drive ## Add Network Drive
add-network-drive = Ajouter un lecteur réseau add-network-drive = Ajouter un lecteur réseau
connect = Connecter connect = Se connecter
connect-anonymously = Se connecter anonymement connect-anonymously = Se connecter anonymement
connecting = Connexion... connecting = Connexion...
domain = Domaine domain = Domaine
@ -206,8 +206,8 @@ network-drive-schemes =
File Transfer Protocol,ftp:// or ftps:// File Transfer Protocol,ftp:// or ftps://
Network File System,nfs:// Network File System,nfs://
Server Message Block,smb:// Server Message Block,smb://
SSH File Transfer Protocol,sftp:// or ssh:// SSH File Transfer Protocol,sftp:// ou ssh://
WebDav,dav:// or davs:// WebDAV,dav:// ou davs://
network-drive-error = Impossible d'accéder au lecteur réseau network-drive-error = Impossible d'accéder au lecteur réseau
password = Mot de passe password = Mot de passe
remember-password = Se souvenir du mot de passe remember-password = Se souvenir du mot de passe
@ -222,7 +222,7 @@ history = Historique
no-history = Aucun élément dans l'historique. no-history = Aucun élément dans l'historique.
pending = En attente pending = En attente
progress = { $percent }% progress = { $percent }%
progress-cancelled = { $percent }%, annulation progress-cancelled = { $percent }%, annulé
progress-paused = { $percent }%, en pause progress-paused = { $percent }%, en pause
failed = Échoué failed = Échoué
complete = Terminé complete = Terminé
@ -259,7 +259,7 @@ deleted =
[one] élément supprimé [one] élément supprimé
*[other] éléments supprimés *[other] éléments supprimés
} depuis { trash } } depuis { trash }
emptying-trash = { trash } en cours de nettoyage ({ $progress })... emptying-trash = Nettoyage en cours de { trash } ({ $progress })...
emptied-trash = { trash } vidée emptied-trash = { trash } vidée
extracting = extracting =
Extraction de { $items } { $items -> Extraction de { $items } { $items ->
@ -271,7 +271,7 @@ extracted =
[one] élément extrait [one] élément extrait
*[other] éléments extraits *[other] éléments extraits
} depuis "{ $from }" vers "{ $to }" } depuis "{ $from }" vers "{ $to }"
setting-executable-and-launching = Paramétrage de "{ $name }" comme exécutable et prêt à être lancé setting-executable-and-launching = Définition de "{ $name }" comme exécutable et lancement en cours
set-executable-and-launched = Défini "{ $name }" comme exécutable et lancé set-executable-and-launched = Défini "{ $name }" comme exécutable et lancé
moving = moving =
Déplacement de { $items } { $items -> Déplacement de { $items } { $items ->
@ -282,7 +282,7 @@ moved =
{ $items } { $items -> { $items } { $items ->
[one] élément déplacé [one] élément déplacé
*[other] éléments déplacés *[other] éléments déplacés
} de "{ $from }" à "{ $to }" } de "{ $from }" vers "{ $to }"
permanently-deleting = permanently-deleting =
Suppression définitive de { $items } { $items -> Suppression définitive de { $items } { $items ->
[one] élément [one] élément
@ -310,7 +310,7 @@ unknown-folder = Dossier inconnu
## Open with ## Open with
menu-open-with = Ouvrir avec... menu-open-with = Ouvrir avec...
default-app = { $name } (défaut) default-app = { $name } (par défaut)
## Show details ## Show details
@ -352,7 +352,7 @@ open-in-terminal = Ouvrir dans le terminal
edit-desktop-entry = Modifier le fichier .desktop edit-desktop-entry = Modifier le fichier .desktop
move-to-trash = Déplacer vers la corbeille move-to-trash = Déplacer vers la corbeille
restore-from-trash = Restaurer depuis la corbeille restore-from-trash = Restaurer depuis la corbeille
remove-from-sidebar = Supprimer de la barre latérale remove-from-sidebar = Retirer de la barre latérale
sort-by-name = Trier par nom sort-by-name = Trier par nom
sort-by-modified = Trier par date de modification sort-by-modified = Trier par date de modification
sort-by-size = Trier par taille sort-by-size = Trier par taille
@ -415,11 +415,11 @@ removing-from-recents =
*[other] éléments *[other] éléments
} de { recents } } de { recents }
other-apps = Autres applications other-apps = Autres applications
set-permissions = Définir les permissions pour "{ $name }" à { $mode } set-permissions = Permissions définies pour "{ $name }" à { $mode }
repository = Dépôt repository = Dépôt
support = Support support = Support
eject = Éjecter eject = Éjecter
remove-from-recents = Supprimer des récents remove-from-recents = Retirer des récents
empty-trash-title = Vider la corbeille ? empty-trash-title = Vider la corbeille ?
setting-permissions = Définition des permissions pour "{ $name }" à { $mode } setting-permissions = Définition des permissions pour "{ $name }" à { $mode }
removed-from-recents = removed-from-recents =
@ -428,9 +428,9 @@ removed-from-recents =
*[other] éléments supprimés *[other] éléments supprimés
} de { recents } } de { recents }
progress-failed = { $percent }%, échec progress-failed = { $percent }%, échec
type-to-search-select = Sélectionne le premier fichier ou dossier qui convient type-to-search-select = Sélectionne le premier fichier ou dossier correspondant
pasted-image = Image collée pasted-image = Image collée
pasted-text = Text collé pasted-text = Texte collé
pasted-video = Vidéo collée pasted-video = Vidéo collée
copy-to-title = Sélectionner la destination de la copie copy-to-title = Sélectionner la destination de la copie
copy-to-button-label = Copier copy-to-button-label = Copier
@ -453,3 +453,9 @@ context-action-confirm-warning =
run = Exécuter run = Exécuter
context-action = Action contextuelle context-action = Action contextuelle
rename-confirm = Renommer rename-confirm = Renommer
calculate = Calculer
error = Erreur
checksum = Somme de contrôle { $kind }
sidebar-label = Libellé de la barre latérale
search-application = Chercher par nom d'appli
change-sidebar-label = Changer le libellé de la barre latérale

View file

@ -307,7 +307,7 @@ single-click = Cliceáil amháin le hoscailt
### Appearance ### Appearance
appearance = Cuma appearance = Dealramh
theme = Téama theme = Téama
match-desktop = Meaitseáil deasc match-desktop = Meaitseáil deasc
dark = Dorcha dark = Dorcha
@ -443,3 +443,6 @@ context-action-confirm-warning =
}. }.
run = Rith run = Rith
rename-confirm = Athainmnigh rename-confirm = Athainmnigh
checksum = { $kind } suim sheiceála
calculate = Ríomh
error = Earráid

333
i18n/gl/cosmic_files.ftl Normal file
View file

@ -0,0 +1,333 @@
name = Nome
save = Gardar
cancel = Cancelar
run = Executar
delete = Eliminar
repository = Repositorio
support = Soporte
settings = Axustes
appearance = Apariencia
theme = Tema
match-desktop = Axustar ao escritorio
dark = Escuro
light = Claro
replace = Remplazar
file = Ficheiro
new-window = Nova xanela
quit = Pechar
edit = Editar
undo = Desfacer
cut = Cortar
copy = Copiar
paste = Pegar
select-all = Seleccionar todo
view = Ver
zoom-in = Aumentar Zoom
default-size = Tamaño por defecto
zoom-out = Afastar Zoom
menu-settings = Axustes...
cosmic-files = Ficheiros COSMIC
comment = Xestor de ficheiros para o escritorio COSMIC
keywords = Cartafol;Xestor;
empty-folder = Cartafol baleiro
empty-folder-hidden = Cartafol baleiro (contén elementos agochados)
no-results = Non se atoparon resultados
filesystem = Sistema de ficheiros
home = Inicio
networks = Redes
notification-in-progress = Operacións de ficheiros en progreso
trash = Lixo
recents = Recentes
today = Hoxe
desktop-view-options = Axustes da vista do escritorio…
show-on-desktop = Amosar no Escritorio
desktop-folder-content = Contido do cartafol Escritorio
mounted-drives = Unidades montadas
trash-folder-icon = Icona do cartafol do Lixo
icon-size-and-spacing = Tamaño e espazado das iconas
icon-size = Tamaño de icona
grid-spacing = Espazado da grella
modified = Modificado
trashed-on = No lixo
size = Tamaño
details = Detalles
dismiss = Descartar a mensaxe
operations-running =
{ $running } { $executandose ->
[unha] operacion
*[outra] operacions
} executandose ({ $percent }%)...
operations-running-finished =
{ $running } { $executandose ->
[unha] operacion
*[outra] operacions
} executandose ({ $percent }%), { $finished } rematadas...
pause = Pausa
resume = Retomar
create-archive = Crear arquivo comprimido
copy-to-title = Seleccionar o destino da copia
copy-to-button-label = Copiar
extract-password-required = Contrasinal necesario
extract-to = Extraer en…
extract-to-title = Extraer a cartafol
empty-trash = Baleirar Lixo
empty-trash-title = Baleirar o lixo?
empty-trash-warning = Os elementos do cartafol do lixo eliminaranse de forma permanente
mount-error = Non é posible acceder a unidade
move-to-title = Seleccionar o destino do movemento
move-to-button-label = Mover
create-new-file = Crear un novo ficheiro
create-new-folder = Crear novo cartafol
file-name = Nome do ficheiro
folder-name = Nome do cartafol
file-already-exists = Existe un ficheiro co mesmo nome
folder-already-exists = Existe un cartafol co mesmo nome
name-hidden = Os nomes que comecen por "." agocharanse
name-invalid = O nome non pode ser "{ $filename }"
name-no-slashes = O nome non pode conter barras
create = Crear
open = Abrir
open-file = Abrir ficheiro
open-folder = Abrir cartafol
open-in-new-tab = Abrir nunha nova lapela
open-in-new-window = Abrir nunha nova xanela
open-item-location = Abrir ubicación do ítem
open-multiple-files = Abrir multiples ficheiros
open-multiple-folders = Abrir multiples cartafoles
save-file = Gardar ficheiro
open-with-title = Como queres abrir «{ $name }»?
browse-store = Atopar { $store }
other-apps = Outras aplicacións
related-apps = Aplicacións relacionadas
context-action = Acción contextual
context-action-confirm-title = Executar "{ $name }"?
context-action-confirm-warning =
Esto vaise executar en { $items } { $items ->
[un] item
*[outro] items
}.
selected-items = Os { $items } elementos seleccionados
permanently-delete-question = Eliminar permanentemente?
permanently-delete-warning = { $target } será eliminado permanentemente. Esta acción non se poderá desfacer.
rename-file = Renomear ficheiro
rename-folder = Renomear cartafol
rename-confirm = Renomear
replace-title = "{ $filename }" xa existe nesta ubicación
replace-warning = Queres substituílo polo que estás a gardar? Ao substituílo sobrescribirase o seu contido.
replace-warning-operation = Queres substituílo? Ao substituílo sobrescribirase o seu contido.
original-file = Ficheiro orixinal
replace-with = Remplazar con
apply-to-all = Aplicar a todo
keep-both = Manter ambos
skip = Saltar
set-executable-and-launch = Marcar como executable e lanzalo
set-executable-and-launch-description = Queres marcar «{ $name }» como executábel e lanzalo?
set-and-launch = Marcar e executar
open-with = Abrir con
owner = Propietario
group = Grupo
other = Outro
mixed = Mesturar
none = Ningún
execute-only = Só execución
write-only = Só escritura
write-execute = Escritura e execución
read-only = Só lectura
read-execute = Lectura e execución
read-write = Lectura e escritura
read-write-execute = Lectura, escritura e execución
favorite-path-error = Erro abrindo o directorio
favorite-path-error-description =
Non foi posíbel abrir «{ $path }».
É posíbel que «{ $path }» non exista ou que non teñas permisos para abrilo.
Queres retiralo da barra lateral?
remove = Eliminar
keep = Manter
add-network-drive = Engadir unidade de rede
connect = Conectar
connect-anonymously = Conectar de xeito anonimo
connecting = Conectando...
domain = Dominio
enter-server-address = Introducir a dirección do servidor
network-drive-description =
Os enderezos dos servidores inclúen un prefixo de protocolo e un enderezo.
Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1]
network-drive-schemes =
Protocolos dispoñíbeis,Prefixo
AppleTalk,afp://
Protocolo de transferencia de ficheiros,ftp:// ou ftps://
Sistema de ficheiros en rede,nfs://
Server Message Block,smb://
Protocolo de transferencia de ficheiros por SSH,sftp:// ou ssh://
WebDAV,dav:// ou davs://
network-drive-error = Non foi posible acceder a unidade de rede
password = Contrasinal
remember-password = Recordar o contrasinal
try-again = Voltar a intentar
username = Nome de usuario
cancelled = Cancelado
edit-history = Editar historial
history = Historial
no-history = Non hai elementos no historial.
pending = Pendente
progress = { $percent }%
progress-cancelled = { $percent }%, cancelado
progress-failed = { $percent }%, fallou
progress-paused = { $percent }%, pausado
failed = Fallou
complete = Completado
compressing =
Comprimindo { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }» ({ $progress })...
compressed =
Comprimidos { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }»
copy_noun = Copiar
pasted-image = Imaxe pegada
pasted-text = Texto Pegado
pasted-video = Video pegado
creating = Creando "{ $name }" en "{ $parent }"
created = Creado "{ $name }" en "{ $parent }"
copying =
Copiando { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }» ({ $progress })...
copied =
Copiados { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }»
deleted =
Eliminando { $items } { $items ->
[one] elemento
*[other] elementos
} do lixo ({ $progress })
emptying-trash = Baleirando { trash } ({ $progress })...
emptied-trash = Baleirado o { trash }
extracting =
Extraendo { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }» ({ $progress })...
extracted =
Extraídos { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }»
setting-executable-and-launching = Marcando «{ $name }» como executábel e executándoo
set-executable-and-launched = «{ $name }» marcouse como executábel e executouse
setting-permissions = Establecendo os permisos de «{ $name }» en { $mode }
set-permissions = Establecéronse os permisos de «{ $name }» en { $mode }
moving =
Movendo { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }» ({ $progress })...
moved =
Movidos { $items } { $items ->
[one] elemento
*[other] elementos
} de «{ $from }» a «{ $to }»
permanently-deleting =
Eliminando permanentemente { $items } { $items ->
[one] elemento
*[other] elementos
}
permanently-deleted =
Eliminados permanentemente { $items } { $items ->
[one] elemento
*[other] elementos
}
removing-from-recents =
Retirando { $items } { $items ->
[one] elemento
*[other] elementos
} dos { recents }
removed-from-recents =
Retirado { $items } { $items ->
[one] elemento
*[other] elementos
} de { recents }
renaming = Renomeando "{ $from }" a "{ $to }"
renamed = Renomeado "{ $from }" a "{ $to }"
restoring =
Restaurando { $items } { $items ->
[one] elemento
*[other] elementos
} de { trash } ({ $progress })...
restored =
Restaurado { $items } { $items ->
[one] elemento
*[other] elementos
} de { trash }
unknown-folder = cartafol descoñecido
menu-open-with = Abrir con...
default-app = { $name } (por defecto)
show-details = Amosar detalles
type = Tipo: { $mime }
items = Elementos: { $items }
item-size = Tamaño: { $size }
item-created = Creado: { $created }
item-modified = Modificado: { $modified }
item-accessed = Accedido: { $accessed }
calculating = Calculando...
checksum = { $kind } checksum
calculate = Calculado
error = Erro
single-click = Clic único para abrir
show-recents = Cartafol de recentes no panel lateral
type-to-search = Escribe para atopar
type-to-search-recursive = Busca no cartafol actual e en todos os subcartafoles
type-to-search-enter-path = Introduce a ruta do cartafol ou ficheiro
type-to-search-select = Selecciona o primeiro ficheiro ou cartafol coincidente
add-to-sidebar = Engadir o panel lateral
clear-recents-history = Limpar historial de recentes
compress = Comprimir...
copy-to = Copiar a...
delete-permanently = Eliminar permanentemente
eject = Expulsar
extract-here = Extraer
new-file = Novo ficheiro...
new-folder = Novo cartafol...
open-in-terminal = Abrir no terminal
move-to = Mover a...
move-to-trash = Mover ao lixo
restore-from-trash = Recuperar do lixo
remove-from-sidebar = Eliminar do panel lateral
sort-by-name = Ordenar por nome
sort-by-modified = Ordenar por modificación
sort-by-size = Ordenar por tamaño
sort-by-trashed = Ordenar pola data de eliminación
remove-from-recents = Eliminar de recentes
change-wallpaper = Cambiar fondo de pantalla...
desktop-appearance = Apariencia do Escritorio...
display-settings = Axustes de pantalla...
new-tab = Nova lapela
reload-folder = Recargar cartafol
rename = Renomear...
close-tab = Pechar lapela
copy-path = Copiar ruta
grid-view = Vista de grella
list-view = Vista de lista
show-hidden-files = Amosar ficheiros ocultos
list-directories-first = Listar primeiro os directorios
gallery-preview = Vista previa da galería
menu-about = Sobre Ficheiros COSMIC...
sort = Ordenar
sort-a-z = A-Z
sort-z-a = Z-A
sort-newest-first = Novos primeiro
sort-oldest-first = Antigos primeiro
sort-smallest-to-largest = Do máis pequeno ao máis grande
sort-largest-to-smallest = Do máis grande ao máis pequeno
deleting =
Eliminando { $items } { $items ->
[one] elemento
*[other] elementos
} da { trash } ({ $progress })...

View file

@ -5,7 +5,7 @@ no-results = कोई परिणाम नहीं
filesystem = फाइल सिस्टम filesystem = फाइल सिस्टम
home = होम home = होम
networks = नेटवर्क्स networks = नेटवर्क्स
notification-in-progress = फाइल संचालन प्रगति पर है। notification-in-progress = फ़ाइल संचालन प्रगति पर हैं
trash = कचरा trash = कचरा
recents = हाल के recents = हाल के
undo = पूर्ववत करें undo = पूर्ववत करें
@ -33,8 +33,8 @@ create-archive = संग्रह बनाएँ
## Empty Trash Dialog ## Empty Trash Dialog
empty-trash = कचरा खाली करें empty-trash = रद्दी साफ़ करें
empty-trash-warning = क्या आप वाकई कचरे में सभी आइटमों को स्थायी रूप से हटाना चाहते हैं? empty-trash-warning = रद्दी फ़ोल्डर में मौजूद आइटम स्थायी रूप से हटा दिए जाएंगे
## New File/Folder Dialog ## New File/Folder Dialog
@ -42,11 +42,11 @@ create-new-file = नई फाइल बनाएँ
create-new-folder = नया फ़ोल्डर बनाएँ create-new-folder = नया फ़ोल्डर बनाएँ
file-name = फाइल का नाम file-name = फाइल का नाम
folder-name = फ़ोल्डर का नाम folder-name = फ़ोल्डर का नाम
file-already-exists = इसी नाम की फाइल पहले से मौजूद है। file-already-exists = उस नाम की फ़ाइल पहले से मौजूद है
folder-already-exists = इसी नाम का फ़ोल्डर पहले से मौजूद है। folder-already-exists = उस नाम का फ़ोल्डर पहले से मौजूद है
name-hidden = "." से शुरू होने वाले नाम छिपे रहेंगे। name-hidden = "." से शुरू होने वाले नाम छिपे होंगे
name-invalid = नाम "{ $filename }" मान्य नहीं है। name-invalid = नाम "{ $filename }" नहीं हो सकता
name-no-slashes = नाम में स्लैश का उपयोग नहीं किया जा सकता है। name-no-slashes = नाम में स्लैश नहीं हो सकते
## Open/Save Dialog ## Open/Save Dialog
@ -75,8 +75,8 @@ rename-folder = फ़ोल्डर का नाम बदलें
## Replace Dialog ## Replace Dialog
replace = प्रतिस्थापित करें replace = बदलें
replace-title = { $filename } पहले से इस स्थान पर मौजूद है। replace-title = { $filename } पहले से इस स्थान पर मौजूद है
replace-warning = क्या आप इसे प्रतिस्थापित करना चाहते हैं? यदि प्रतिस्थापित किया गया, तो मौजूदा फ़ाइल को ओवरराइट किया जाएगा। replace-warning = क्या आप इसे प्रतिस्थापित करना चाहते हैं? यदि प्रतिस्थापित किया गया, तो मौजूदा फ़ाइल को ओवरराइट किया जाएगा।
replace-warning-operation = क्या आप इसे बदलना चाहते हैं? प्रतिस्थापित करने पर मौजूदा फ़ाइल ओवरराइट हो जाएगी। replace-warning-operation = क्या आप इसे बदलना चाहते हैं? प्रतिस्थापित करने पर मौजूदा फ़ाइल ओवरराइट हो जाएगी।
original-file = मूल फ़ाइल original-file = मूल फ़ाइल
@ -211,13 +211,13 @@ show-details = विवरण दिखाएँ
## Settings ## Settings
settings = सेटिंग settings = सेटिंग्स
### Appearance ### Appearance
appearance = रूप appearance = रुप-रंग
theme = थीम theme = प्रसंग
match-desktop = सिस्टम सेटिंग्स से मेल खाएँ match-desktop = डेस्कटॉप से मेल खाएँ
dark = डार्क dark = डार्क
light = लाइट light = लाइट
# Context menu # Context menu
@ -251,27 +251,27 @@ new-tab = नया टैब
new-window = नई विंडो new-window = नई विंडो
rename = नाम बदलें... rename = नाम बदलें...
close-tab = टैब बंद करें close-tab = टैब बंद करें
quit = बंद करें quit = बाहर जाएँ
## Edit ## Edit
edit = संपादित करें edit = संपादन
cut = कट करें cut = काटें
copy = नकल करें copy = कॉपी करें
paste = चिपकाएँ paste = चिपकाएँ
select-all = सभी चुनें select-all = सभी चुनें
## View ## View
zoom-in = ज़ूम इन करें zoom-in = बड़ा करें
default-size = डिफ़ॉल्ट आकार default-size = मूल आकार
zoom-out = ज़ूम आउट करें zoom-out = छोटा करें
view = दृश्य view = दृश्य
grid-view = ग्रिड दृश्य grid-view = ग्रिड दृश्य
list-view = सूची दृश्य list-view = सूची दृश्य
show-hidden-files = छिपी हुई फाइलें दिखाएँ show-hidden-files = छिपी हुई फाइलें दिखाएँ
list-directories-first = सबसे पहले डाइरेक्ट्री दिखाएँ list-directories-first = सबसे पहले डाइरेक्ट्री दिखाएँ
menu-settings = सेटिंग्स... menu-settings = सेटिंग्स..।
menu-about = कॉस्मिक फाइल्स के बारे में... menu-about = कॉस्मिक फाइल्स के बारे में...
## Sort ## Sort
@ -284,4 +284,98 @@ sort-oldest-first = पुराने से नए
sort-smallest-to-largest = छोटे से बड़े sort-smallest-to-largest = छोटे से बड़े
sort-largest-to-smallest = बड़े से छोटे sort-largest-to-smallest = बड़े से छोटे
repository = रिपॉजिटरी repository = रिपॉजिटरी
support = सहायता support = समर्थन
read-execute = पढ़ें और निष्पादित करें
deleted =
{ trash } से { $items } { $items ->
[one] आइटम मिटाया गया
*[other] आइटम मिटाए गए
}
dismiss = संदेश खारिज करें
favorite-path-error = निर्देशिका खोलने में त्रुटि
progress = { $percent }%
related-apps = संबंधित ऐप्स
removing-from-recents =
{ recents } से { $items } { $items ->
[one] आइटम हटाया जा रहा है
*[other] आइटम हटाए जा रहे हैं
}
remove = हटाएँ
read-write-execute = पढ़ें, लिखें और निष्पादित करें
other-apps = अन्य ऐप्स
pause = विराम
keep = रखें
permanently-deleting =
{ $items } { $items ->
[one] आइटम स्थायी रूप से मिटाया जा रहा है
*[other] आइटम स्थायी रूप से मिटाए जा रहे हैं
}
read-write = पढ़ें और लिखें
none = कोई नहीं
resume = फिर से शुरू करें
grid-spacing = ग्रिड स्पेसिंग
extract-to = इस रूप में निकालें..।
delete = हटाएं
read-only = केवल पढ़ने के लिए
deleting =
{ trash } से { $items } { $items ->
[one] आइटम मिटाया जा रहा है
*[other] आइटम मिटाए जा रहे हैं
} ({ $progress })..।
execute-only = केवल निष्पादित करें
details = विवरण
mount-error = ड्राइव तक पहुँचने में असमर्थ
removed-from-recents =
{ recents } से { $items } { $items ->
[one] आइटम हटाया गया
*[other] आइटम हटाए गए
}
progress-paused = { $percent }%, रुका हुआ
cancelled = रद्द किया गया
operations-running-finished =
{ $running } { $running ->
[one] ऑपरेशन
*[other] ऑपरेशन
} चल रहा है ({ $percent }%), { $finished } समाप्त..।
progress-failed = { $percent }%, विफल
extract-to-title = फ़ोल्डर में निकालें
open-with = इससे खोलें
permanently-deleted =
{ $items } { $items ->
[one] आइटम स्थायी रूप से मिटाया गया
*[other] आइटम स्थायी रूप से मिटाए गए
}
write-execute = लिखें और निष्पादित करें
extract-password-required = पासवर्ड आवश्यक है
progress-cancelled = { $percent }%, रद्द किया गया
write-only = केवल लिखने के लिए
permanently-delete-warning = { $target } को स्थाई रूप से हटा दिया जाएगा। इस कार्रवाई को पूर्ववत नहीं किया जा सकता है।
favorite-path-error-description =
"{ $path }" को खोला नहीं जा सकता
"{ $path }" मौजूद नहीं हो सकता है या आपके पास इसे खोलने की अनुमति नहीं हो सकती है
क्या आप इसे साइडबार से हटाना चाहते हैं?
empty-trash-title = रद्दी साफ़ करें?
permanently-delete-question = स्थाई रूप से हटाएं?
operations-running =
{ $running } { $running ->
[one] ऑपरेशन
*[other] ऑपरेशन
} चल रहा है ({ $percent }%)..।
copy-to-title = कॉपी गंतव्य चुनें
copy-to-button-label = कॉपी
move-to-title = मूव गंतव्य चुनें
move-to-button-label = मूव
context-action = संदर्भ क्रिया
context-action-confirm-title = "{ $name }" चलाएँ?
context-action-confirm-warning =
यह { $items } { $items ->
[one] आइटम
*[other] आइटम
} पर चलेगा।
run = चलाएँ
rename-confirm = नाम बदलें
mixed = मिश्रित
pasted-image = चिपकाई गई छवि
pasted-text = चिपकाया गया पाठ
pasted-video = चिपकाया गया वीडियो

View file

@ -326,7 +326,7 @@ default-app = { $name } (alapértelmezett)
show-details = Részletek megjelenítése show-details = Részletek megjelenítése
type = Típus: { $mime } type = Típus: { $mime }
items = Elemek: { $items } items = Elemek száma: { $items }
item-size = Méret: { $size } item-size = Méret: { $size }
item-created = Létrehozva: { $created } item-created = Létrehozva: { $created }
item-modified = Módosítva: { $modified } item-modified = Módosítva: { $modified }
@ -446,3 +446,9 @@ context-action-confirm-warning =
} fog lefutni. } fog lefutni.
run = Futtatás run = Futtatás
rename-confirm = Átnevezés rename-confirm = Átnevezés
checksum = { $kind }-ellenőrzőösszeg
calculate = Számítás
error = Hiba
sidebar-label = Oldalsávcímke
search-application = Keresés alkalmazásnév alapján
change-sidebar-label = Oldalsávcímke módosítása

View file

@ -249,7 +249,7 @@ settings = Pengaturan
single-click = Klik sekali untuk membuka single-click = Klik sekali untuk membuka
appearance = Tampilan appearance = Tampilan
theme = Tema theme = Tema
match-desktop = Cocokkan desktop match-desktop = Cocok desktop
dark = Gelap dark = Gelap
light = Terang light = Terang
type-to-search = Ketik untuk mencari type-to-search = Ketik untuk mencari
@ -328,3 +328,9 @@ context-action-confirm-warning =
*[other] item *[other] item
}. }.
rename-confirm = Ganti nama rename-confirm = Ganti nama
checksum = { $kind } checksum
calculate = Hitung
error = Galat
sidebar-label = Label bilah sisi
search-application = Cari berdasarkan nama aplikasi
change-sidebar-label = Ubah label bilah sisi

View file

@ -1,5 +1,5 @@
cosmic-files = COSMIC Files cosmic-files = COSMIC Files
comment = File manager di COSMIC comment = File manager per il desktop COSMIC
keywords = File;Archivi;Cartelle;Explorer; keywords = File;Archivi;Cartelle;Explorer;
empty-folder = Cartella vuota empty-folder = Cartella vuota
empty-folder-hidden = Cartella vuota (con elementi nascosti) empty-folder-hidden = Cartella vuota (con elementi nascosti)
@ -7,7 +7,7 @@ no-results = Nessun risultato trovato
filesystem = Filesystem filesystem = Filesystem
home = Home home = Home
networks = Reti networks = Reti
notification-in-progress = Operazioni sui file in corso. notification-in-progress = Operazioni sui file in corso
trash = Cestino trash = Cestino
recents = Recenti recents = Recenti
undo = Annulla undo = Annulla
@ -322,7 +322,7 @@ single-click = Click singolo per aprire
appearance = Aspetto appearance = Aspetto
theme = Tema theme = Tema
match-desktop = Sistema match-desktop = Abbina al desktop
dark = Scuro dark = Scuro
light = Chiaro light = Chiaro
@ -422,3 +422,4 @@ removed-from-recents =
[one] elemento [one] elemento
*[other] elementi *[other] elementi
} da { recents } } da { recents }
run = Esegui

View file

@ -20,7 +20,7 @@ name = Isem
modified = Ittusnifel modified = Ittusnifel
trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman trashed-on = Yettwakkes ɣer tqecwalt n yiḍumman
size = Tiddi size = Tiddi
details = Talqayt details = Ifatusen
pause = Serǧu pause = Serǧu
resume = Kemmel resume = Kemmel
create-archive = Snulfu-d aɣbaṛ create-archive = Snulfu-d aɣbaṛ
@ -40,7 +40,7 @@ file-already-exists = Afaylu s yisem-agi yella yakan
folder-already-exists = Akaram s yisem-agi yella yakan folder-already-exists = Akaram s yisem-agi yella yakan
name-hidden = Ismawen ibeddun s "." ad ttwaffren name-hidden = Ismawen ibeddun s "." ad ttwaffren
name-invalid = Isem ur yezmir ara ad yili "{ $filename }" name-invalid = Isem ur yezmir ara ad yili "{ $filename }"
cancel = Sefsex cancel = Semmet
create = Snulfu-d create = Snulfu-d
open = Ldi open = Ldi
open-file = Ldi afaylu open-file = Ldi afaylu
@ -72,7 +72,7 @@ comment = Amsefrak n yifuyla i tnarit COSMIC
keywords = Akaram;Amsefrak; keywords = Akaram;Amsefrak;
delete = kkes delete = kkes
replace = Semselsi replace = Semselsi
support = Tallalt support = Tallelt
settings = Iɣewwaṛen settings = Iɣewwaṛen
appearance = Timeẓri appearance = Timeẓri
theme = Asentel theme = Asentel
@ -86,10 +86,10 @@ zoom-out = Asemẓi
view = Wali view = Wali
menu-settings = Iɣewwaṛen… menu-settings = Iɣewwaṛen…
save = Sekles save = Sekles
match-desktop = Amṣada d tnarit match-desktop = Semṣada akked tnarit
file = Afaylu file = Afaylu
new-window = Asfaylu amaynut new-window = Asfaylu amaynut
quit = Tuffɣa quit = Ffeɣ
edit = Ẓreg edit = Ẓreg
cut = Gzem cut = Gzem
copy = Nɣel copy = Nɣel

View file

@ -22,7 +22,7 @@ name = Аты
modified = Өзгертілген modified = Өзгертілген
trashed-on = Қоқыс шелегіне тасталған trashed-on = Қоқыс шелегіне тасталған
size = Өлшемі size = Өлшемі
details = Ақпараты details = Толық ақпарат
dismiss = Хабарламаны елемеу dismiss = Хабарламаны елемеу
operations-running = operations-running =
{ $running } { $running -> { $running } { $running ->
@ -246,10 +246,10 @@ item-accessed = Қол жеткізілген: { $accessed }
calculating = Есептеу... calculating = Есептеу...
settings = Баптаулар settings = Баптаулар
single-click = Ашу үшін бір рет шерту single-click = Ашу үшін бір рет шерту
appearance = Сыртқы түрі appearance = Сыртқы көрініс
theme = Тақырып theme = Тема
match-desktop = Жұмыс үстеліне сәйкес келу match-desktop = Жұмыс үстеліне сәйкес болу
dark = Күңгірт dark = Қою
light = Ашық light = Ашық
type-to-search = Іздеу үшін теру type-to-search = Іздеу үшін теру
type-to-search-recursive = Ағымдағы бума мен барлық ішкі бумаларды іздейді type-to-search-recursive = Ағымдағы бума мен барлық ішкі бумаларды іздейді
@ -328,3 +328,6 @@ context-action-confirm-warning =
}. }.
run = Орындау run = Орындау
rename-confirm = Атын өзгерту rename-confirm = Атын өзгерту
checksum = { $kind } бақылау сомасы
calculate = Есептеу
error = Қате

View file

@ -30,10 +30,10 @@ new-window = Çarçoveya nû
remove = Rake remove = Rake
zoom-out = Dûr bike zoom-out = Dûr bike
compressing = compressing =
{ $items } tê guvaştin { $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })... } tê guvaştin ji "{ $from }" bo "{ $to }" ({ $progress })...
move-to-trash = Bilivîne bo jêbirdankê move-to-trash = Bilivîne bo jêbirdankê
menu-about = Derbarê Pelên COSMIC... menu-about = Derbarê Pelên COSMIC...
file-already-exists = Pelek bi heman navî jixwe heye file-already-exists = Pelek bi heman navî jixwe heye
@ -42,10 +42,10 @@ open-multiple-files = Gelek pelan veke
default-size = Mezinahiya berdest default-size = Mezinahiya berdest
name-hidden = Navên ku bi "." tê destpêkirin wê werin veşartin name-hidden = Navên ku bi "." tê destpêkirin wê werin veşartin
extracted = extracted =
{ $items } hate derxistin { $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" } hate derxistin ji "{ $from }" bo "{ $to }"
create-new-folder = Peldanka nû biafrîne create-new-folder = Peldanka nû biafrîne
folder-already-exists = Peldankek bi heman navî jixwe heye folder-already-exists = Peldankek bi heman navî jixwe heye
create-archive = Erşîvê biafrîne create-archive = Erşîvê biafrîne
@ -79,10 +79,10 @@ none = Ne yek
no-results = Tu encam nehatin dîtin no-results = Tu encam nehatin dîtin
theme = Rûkar theme = Rûkar
compressed = compressed =
{ $items } hate guvaştin{ $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" } hate guvaştin ji "{ $from }" bo "{ $to }"
appearance = Xuyang appearance = Xuyang
rename-folder = Navê peldankê biguherîne rename-folder = Navê peldankê biguherîne
new-file = Pela nû... new-file = Pela nû...
@ -95,20 +95,20 @@ light = Ronî
extract-to = Derxîne bo... extract-to = Derxîne bo...
add-network-drive = Ajokarê torê tevlî bike add-network-drive = Ajokarê torê tevlî bike
copying = copying =
{ $items } tê jêgirtin{ $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })... } tê jêgirtin ji "{ $from }" bo "{ $to }" ({ $progress })...
create-new-file = Pelê nû biafrîne create-new-file = Pelê nû biafrîne
sort-by-trashed = Li gorî demê rêz bike sort-by-trashed = Li gorî demê rêz bike
support = Piştgirî support = Piştgirî
try-again = Dîsa hewl bide try-again = Dîsa hewl bide
eject = Biavêje eject = Biavêje
copied = copied =
{ $items } hate jêgirtin { $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" } hate jêgirtin ji "{ $from }" bo "{ $to }"
open-in-new-window = Di çarçoveyeke nû de veke open-in-new-window = Di çarçoveyeke nû de veke
empty-folder = Peldanka vala empty-folder = Peldanka vala
empty-trash = Jêbirdankê vala bike empty-trash = Jêbirdankê vala bike
@ -117,14 +117,14 @@ list-directories-first = Pêşî rêgehan lîste bike
folder-name = Navê peldankê folder-name = Navê peldankê
browse-store = { $store } bigere browse-store = { $store } bigere
remove-from-recents = Ji dawiyê rake remove-from-recents = Ji dawiyê rake
paste = Pêve bike paste = Pê ve bike
empty-trash-title = Jêbirdankê vala bike? empty-trash-title = Jêbirdankê vala bike?
menu-settings = Sazkarî... menu-settings = Sazkarî...
moving = moving =
{ $items } tê livandin{ $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" ({ $progress })... } tê livandin ji "{ $from }" bo "{ $to }" ({ $progress })...
change-wallpaper = Wêneyê dîwêr biguhêrîne... change-wallpaper = Wêneyê dîwêr biguhêrîne...
network-drive-description = network-drive-description =
Di nav navnîşanên rajekarê de pêşgira protokolekê û navnîşanek heye. Di nav navnîşanên rajekarê de pêşgira protokolekê û navnîşanek heye.
@ -158,8 +158,8 @@ progress-paused = { $percent }%, hate rawestandin
desktop-view-options = Vebijêrkên dîtina sermaseyê... desktop-view-options = Vebijêrkên dîtina sermaseyê...
show-on-desktop = Li ser sermaseyê nîşan bide show-on-desktop = Li ser sermaseyê nîşan bide
cancelled = Têkbirî cancelled = Têkbirî
new-folder = Peldankan nû... new-folder = Peldanka nû...
match-desktop = Li gorî sermaseyê match-desktop = Bi sermaseyê re li hev bîne
name-no-slashes = Nav nabe ku "/" tê de hebe name-no-slashes = Nav nabe ku "/" tê de hebe
operations-running-finished = operations-running-finished =
{ $running } { $running -> { $running } { $running ->
@ -175,10 +175,10 @@ extract-to-title = Derxîne bo peldankê
icon-size = Mezinahiya îkonê icon-size = Mezinahiya îkonê
open-with-title = Tu çawa dixwazî "{ $name }" vekî? open-with-title = Tu çawa dixwazî "{ $name }" vekî?
extracting = extracting =
{ $items } tê derxistin { $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo"{ $to }" ({ $progress })... } tê derxistin ji "{ $from }" bo"{ $to }" ({ $progress })...
settings = Sazkarî settings = Sazkarî
complete = Qediya complete = Qediya
permanently-delete-question = Bi mayînde jê bibe? permanently-delete-question = Bi mayînde jê bibe?
@ -193,10 +193,10 @@ sort-largest-to-smallest = Mezintir bo biçûktir
restore-from-trash = Ji jêbirdankê vegerîne restore-from-trash = Ji jêbirdankê vegerîne
cut = Jê bike cut = Jê bike
moved = moved =
{ $items } hate livandin { $items -> { $items } { $items ->
[one] hêman [one] hêman
*[other] hêman *[other] hêman
} ji "{ $from }" bo "{ $to }" } hate livandin ji "{ $from }" bo "{ $to }"
progress-cancelled = { $percent }%, hat têkbirin progress-cancelled = { $percent }%, hat têkbirin
open-in-new-tab = Di rûgerê nû de veke open-in-new-tab = Di rûgerê nû de veke
file = Pel file = Pel
@ -220,7 +220,7 @@ trash = Jêbirdank
undo = Vegerîne undo = Vegerîne
today = Îro today = Îro
name = Nav name = Nav
modified = Guhertî modified = Guhertin
size = Mezinahî size = Mezinahî
details = Hûrgilî details = Hûrgilî
pause = Rawestîne pause = Rawestîne
@ -229,7 +229,7 @@ copy-to-title = Rêgeha jêgirtinê hilbijêre
copy-to-button-label = Jê bigire copy-to-button-label = Jê bigire
move-to-title = Rêgeha livandinê hilbijêre move-to-title = Rêgeha livandinê hilbijêre
move-to-button-label = Bilivîne move-to-button-label = Bilivîne
create = Biafrîne create = Afirandin
save = Tomar bike save = Tomar bike
context-action = Çalakiya naverokê context-action = Çalakiya naverokê
context-action-confirm-title = "{ $name }" bixebtîne? context-action-confirm-title = "{ $name }" bixebtîne?
@ -263,7 +263,7 @@ restoring =
} ji { trash } ({ $progress })... } ji { trash } ({ $progress })...
icon-size-and-spacing = Mezinahiya îkonan û valahiya di navbera wan de icon-size-and-spacing = Mezinahiya îkonan û valahiya di navbera wan de
type-to-search-enter-path = Riya rêgehê yan jî pelê têdixîne type-to-search-enter-path = Riya rêgehê yan jî pelê têdixîne
menu-open-with = Veke bi... menu-open-with = Veke bi riya...
original-file = Pelê resen original-file = Pelê resen
read-write-execute = Xwendin, nivîsandin û xebitandin read-write-execute = Xwendin, nivîsandin û xebitandin
calculating = Tê jimartin... calculating = Tê jimartin...
@ -311,7 +311,7 @@ restored =
domain = Navper domain = Navper
sort = Rêz bike sort = Rêz bike
item-accessed = Gihîştin: { $accessed } item-accessed = Gihîştin: { $accessed }
open-with = Veke bi open-with = Veke bi riya
keep-both = Herduyan bihêle keep-both = Herduyan bihêle
permanently-deleted = permanently-deleted =
{ $items } bi mayînde hate jêbirin{ $items -> { $items } bi mayînde hate jêbirin{ $items ->
@ -325,6 +325,12 @@ replace = Biguhêrîne
replace-title = "{ $filename }" jixwe di vê cihê de heye replace-title = "{ $filename }" jixwe di vê cihê de heye
unknown-folder = Peldanka nenas unknown-folder = Peldanka nenas
write-only = Tenê nivîsandin write-only = Tenê nivîsandin
trashed-on = Li Jêbirdankê hatiye avêtin trashed-on = Jêbirî
mixed = Tevlihev mixed = Tevlihev
type-to-search-select = Pel an peldanka yekem a lihevhatî hildibijêre type-to-search-select = Pel an peldanka yekem a lihevhatî hildibijêre
calculate = Bijmêre
error = Şaşetî
checksum = Tevahiya { $kind } kontrol bike
sidebar-label = Nîşanka benda kêlekê
search-application = Li gorî navê sepanê bigere
change-sidebar-label = Nîşanka benda kêlekê biguhêrîne

View file

@ -278,3 +278,12 @@ move-to-button-label = 이동
clear-recents-history = 최근 기록 비우기 clear-recents-history = 최근 기록 비우기
copy-path = 복사 경로 copy-path = 복사 경로
move-to-title = 이동 위치 선택 move-to-title = 이동 위치 선택
copy-to-title = 복사 경로 선택
run = 실행
rename-confirm = 이름 변경
mixed = 혼합됨
pasted-image = 붙여넣은 이미지
pasted-text = 붙여넣은 텍스트
pasted-video = 붙여넣은 영상
copy-to = 복사하기...
move-to = 이동하기...

View file

@ -244,7 +244,7 @@ item-created = Sukurtas: { $created }
item-modified = Modifikuota: { $modified } item-modified = Modifikuota: { $modified }
item-accessed = Paskutinė prieiga: { $accessed } item-accessed = Paskutinė prieiga: { $accessed }
calculating = Skaičiuojama... calculating = Skaičiuojama...
settings = Nuostatos settings = Nuostatai
single-click = Vieno paspaudimo atidarymas single-click = Vieno paspaudimo atidarymas
appearance = Išvaizda appearance = Išvaizda
match-desktop = Pagal darbalaukio temą match-desktop = Pagal darbalaukio temą
@ -276,7 +276,7 @@ new-window = Naujas langas
reload-folder = Perkrauti aplanką reload-folder = Perkrauti aplanką
rename = Pervadinti... rename = Pervadinti...
close-tab = Uždaryti skirtuką close-tab = Uždaryti skirtuką
quit = Išeiti quit = Išjungti
edit = Redaguoti edit = Redaguoti
cut = Iškirpti cut = Iškirpti
copy = Kopijuoti copy = Kopijuoti
@ -291,7 +291,7 @@ list-view = Sąrašo išdėstymas
show-hidden-files = Rodyti paslėptus failus show-hidden-files = Rodyti paslėptus failus
list-directories-first = Pirmiau pateikti aplankus list-directories-first = Pirmiau pateikti aplankus
gallery-preview = Galerijos peržiūra gallery-preview = Galerijos peržiūra
menu-settings = Nuostatos... menu-settings = Nuostatai...
menu-about = Apie COSMIC Files... menu-about = Apie COSMIC Files...
sort = Rikiuoti sort = Rikiuoti
sort-a-z = A-Ž sort-a-z = A-Ž
@ -318,3 +318,9 @@ copy-to = Kopijuoti į...
move-to = Perkeltiį į... move-to = Perkeltiį į...
copy-path = Kopijuoti kelią copy-path = Kopijuoti kelią
theme = Stilius theme = Stilius
context-action = Konteksto veiksmas
context-action-confirm-title = Paleisti „{ $name }“?
run = Paleisti
rename-confirm = Pervadinti
calculate = Skaičiuoti
error = Klaida

View file

@ -425,7 +425,7 @@ removed-from-recents =
} uit { recents } verwijderd } uit { recents } verwijderd
remove-from-recents = Uit recente verwijderen remove-from-recents = Uit recente verwijderen
type-to-search-select = Dit selecteert het eerst overeenkomende bestand of map type-to-search-select = Dit selecteert het eerst overeenkomende bestand of map
comment = Bestandsbeheerder voor COSMIC desktop comment = Bestandsbeheerder voor COSMIC
copy-to-title = Kopieerbestemming aanwijzen copy-to-title = Kopieerbestemming aanwijzen
copy-to-button-label = Kopiëren copy-to-button-label = Kopiëren
move-to-button-label = Verplaatsen move-to-button-label = Verplaatsen

View file

@ -0,0 +1,33 @@
appearance = Aparéncia
theme = Tèma
match-desktop = Assortir al burèu
dark = Escur
light = Clar
file = Fichièr
details = Detalhs
name = Nom
cancel = Anullar
repository = Depaus
settings = Configuracion
menu-settings = Configuracion...
quit = Quitar
cosmic-files = Fichièrs de COSMIC
comment = Gestionari de fichièrs pel burèu COSMIC
save-file = Enregistrar lo fichièr
open-with-title = Cossí volètz dobrir « { $name } » ?
other-apps = Autras aplicacions
context-action-confirm-title = Executar « { $name } » ?
run = Executar
delete = Suprimir
replace = Remplaçar
owner = Propietari
group = Grop
write-execute = Escritura e execucion
read-only = Lectura sola
read-execute = Lectura e execucion
read-write = Lectura e execucion
read-write-execute = Lectura, escritura e execucion
remove = Suprimir
connecting = Connexion...
domain = Domeni
password = Senhal

View file

@ -146,7 +146,7 @@ new-tab = ਨਵੀਂ ਟੈਬ
new-window = ਨਵੀਂ ਵਿੰਡੋ new-window = ਨਵੀਂ ਵਿੰਡੋ
rename = ਨਾਂ ਨੂੰ ਬਦਲੋ... rename = ਨਾਂ ਨੂੰ ਬਦਲੋ...
close-tab = ਟੈਬ ਨੂੰ ਬੰਦ ਕਰੋ close-tab = ਟੈਬ ਨੂੰ ਬੰਦ ਕਰੋ
quit = ਬਾਹਰ quit = ਸਮਾਪਤ
edit = ਸੋਧੋ edit = ਸੋਧੋ
cut = ਕੱਟੋ cut = ਕੱਟੋ
copy = ਕਾਪੀ ਕਰੋ copy = ਕਾਪੀ ਕਰੋ
@ -188,9 +188,9 @@ item-accessed = ਪਹੁੰਚ: { $accessed }
calculating = ਗਿਣਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ... calculating = ਗਿਣਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ...
single-click = ਇੱਕ ਵਾਰ ਕਲਿੱਕ ਕਰਕੇ ਖੋਲ੍ਹੋ single-click = ਇੱਕ ਵਾਰ ਕਲਿੱਕ ਕਰਕੇ ਖੋਲ੍ਹੋ
show-recents = ਬਾਹੀ ਵਿੱਚ ਸੱਜਰੇ ਫੋਲਡਰ show-recents = ਬਾਹੀ ਵਿੱਚ ਸੱਜਰੇ ਫੋਲਡਰ
match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਦਾ match-desktop = ਡੈਸਕਟਾਪ ਨਾਲ ਮਿਲਾਓ
dark = ਗੂੜ੍ਹਾ dark = ਗੂੜ੍ਹਾ
light = ਹਲਕਾ light = ਫਿੱਕਾ
type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ type-to-search = ਖੋਜਣ ਲਈ ਲਿਖੋ
type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ type-to-search-recursive = ਮੌਜੂਦਾ ਫੋਲਡਰ ਅਤੇ ਸਭ ਅਧੀਨ-ਫੋਲਡਰਾਂ ਵਿੱਚ ਖੋਜੋ
add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ add-to-sidebar = ਬਾਹੀ ਵਿੱਚ ਜੋੜੋ

View file

@ -1,5 +1,5 @@
cosmic-files = Pliki COSMIC cosmic-files = Pliki COSMIC
comment = Menedżer plików pulpitu COSMIC comment = Menedżer plików dla środowiska COSMIC
keywords = Katalogi;Pliki;Menedżer; keywords = Katalogi;Pliki;Menedżer;
empty-folder = Pusty katalog empty-folder = Pusty katalog
empty-folder-hidden = Pusty katalog (z ukrytymi plikami) empty-folder-hidden = Pusty katalog (z ukrytymi plikami)
@ -27,7 +27,7 @@ modified = Zmodyfikowano
trashed-on = Wyrzucono do kosza trashed-on = Wyrzucono do kosza
size = Rozmiar size = Rozmiar
# Progress footer # Progress footer
details = Detale details = Szczegóły
dismiss = Odrzuć wiadomość dismiss = Odrzuć wiadomość
operations-running = operations-running =
{ $running } bieżące { $running -> { $running } bieżące { $running ->
@ -390,7 +390,7 @@ new-window = Nowe okno
reload-folder = Odśwież katalog reload-folder = Odśwież katalog
rename = Zmień nazwę… rename = Zmień nazwę…
close-tab = Zamknij kartę close-tab = Zamknij kartę
quit = Zamknij quit = Zakończ
## Edit ## Edit
@ -447,3 +447,9 @@ context-action-confirm-warning =
}. }.
run = Uruchom run = Uruchom
rename-confirm = Zmień nazwę rename-confirm = Zmień nazwę
checksum = { $kind } suma kontrolna
calculate = Oblicz
error = Błąd
sidebar-label = Etykieta panelu bocznego
search-application = Wyszukaj po nazwie aplikacji
change-sidebar-label = Zmień etykietę panelu bocznego

View file

@ -109,6 +109,10 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target
rename-file = Renomear arquivo rename-file = Renomear arquivo
rename-folder = Renomear pasta rename-folder = Renomear pasta
## Change Sidebar Label Dialog
sidebar-label = Legenda da barra lateral
## Replace Dialog ## Replace Dialog
replace = Substituir replace = Substituir
@ -350,7 +354,7 @@ light = Estilo claro
### Type to Search ### Type to Search
type-to-search = Digite para pesquisar type-to-search = Digitar para pesquisar
type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas type-to-search-recursive = Pesquisa na pasta atual e em todas as subpastas
type-to-search-enter-path = Insere o caminho do diretório ou arquivo type-to-search-enter-path = Insere o caminho do diretório ou arquivo
# Context menu # Context menu
@ -365,6 +369,7 @@ open-in-terminal = Abrir no terminal
move-to-trash = Mover para a lixeira move-to-trash = Mover para a lixeira
restore-from-trash = Restaurar da lixeira restore-from-trash = Restaurar da lixeira
remove-from-sidebar = Remover da barra lateral remove-from-sidebar = Remover da barra lateral
change-sidebar-label = Alterar legenda da barra lateral
sort-by-name = Ordenar por nome sort-by-name = Ordenar por nome
sort-by-modified = Ordenar por data de modificação sort-by-modified = Ordenar por data de modificação
sort-by-size = Ordenar por tamanho sort-by-size = Ordenar por tamanho
@ -388,7 +393,7 @@ new-window = Nova janela
reload-folder = Recarregar pasta reload-folder = Recarregar pasta
rename = Renomear... rename = Renomear...
close-tab = Fechar aba close-tab = Fechar aba
quit = Sair quit = Encerrar
## Edit ## Edit
@ -446,3 +451,7 @@ context-action-confirm-warning =
}. }.
run = Executar run = Executar
rename-confirm = Renomear rename-confirm = Renomear
checksum = Soma de verificação { $kind }
calculate = Calcular
error = Erro
search-application = Pesquisar por nome de aplicativo

View file

@ -174,6 +174,7 @@ unknown-folder = неизвестная папка
menu-open-with = Открыть с помощью… menu-open-with = Открыть с помощью…
default-app = { $name } (по умолчанию) default-app = { $name } (по умолчанию)
search-application = Поиск по названию приложения
## Show details ## Show details
@ -188,7 +189,7 @@ settings = Параметры
### Appearance ### Appearance
appearance = Оформление appearance = Внешний вид
theme = Тема theme = Тема
match-desktop = Как в системе match-desktop = Как в системе
dark = Тёмная dark = Тёмная
@ -218,7 +219,7 @@ new-tab = Новая вкладка
new-window = Новое окно new-window = Новое окно
rename = Переименовать… rename = Переименовать…
close-tab = Закрыть вкладку close-tab = Закрыть вкладку
quit = Выйти quit = Завершить
## Edit ## Edit
@ -271,8 +272,8 @@ extract-to = Распаковать в…
extract-to-title = Распаковать в папку extract-to-title = Распаковать в папку
mount-error = Не удалось получить доступ к диску mount-error = Не удалось получить доступ к диску
create = Создать create = Создать
open-with-title = Как вы хотите открыть «{ $name }»? open-with-title = Как хотите открыть «{ $name }»?
browse-store = Искать в { $store } browse-store = Перейти в { $store }
other-apps = Другие приложения other-apps = Другие приложения
related-apps = Связанные приложения related-apps = Связанные приложения
selected-items = { $items } выделенных элем. selected-items = { $items } выделенных элем.
@ -320,7 +321,7 @@ type-to-search-enter-path = Ввод пути к каталогу или фай
delete-permanently = Удалить навсегда delete-permanently = Удалить навсегда
eject = Извлечь eject = Извлечь
remove-from-recents = Убрать из недавних remove-from-recents = Убрать из недавних
change-wallpaper = Изменить обои… change-wallpaper = Изменить фон...
desktop-appearance = Параметры оформления… desktop-appearance = Параметры оформления…
display-settings = Параметры экрана… display-settings = Параметры экрана…
reload-folder = Обновить папку reload-folder = Обновить папку
@ -382,11 +383,16 @@ clear-recents-history = Очистить историю недавних
copy-path = Копировать путь copy-path = Копировать путь
mixed = Смешанные mixed = Смешанные
context-action = Контекстная команда context-action = Контекстная команда
context-action-confirm-title = Выполнить «{ $name }»? context-action-confirm-title = Запустить «{ $name }»?
context-action-confirm-warning = context-action-confirm-warning =
Команда затронет { $items } { $items -> Команда затронет { $items } { $items ->
[one] элемент [one] элемент
*[other] элем. *[other] элем.
}. }.
run = Выполнить run = Запустить
rename-confirm = Переименовать rename-confirm = Переименовать
checksum = Хеш-сумма { $kind }
calculate = Вычислить
error = Ошибка
sidebar-label = Подпись в боковой панели
change-sidebar-label = Изменить подпись

View file

@ -0,0 +1,3 @@
cancel = ᱵᱟᱹᱛᱤᱞ
name = ᱧᱩᱛᱩᱢ
save = ᱥᱟᱧᱪᱟᱣ

View file

@ -319,7 +319,7 @@ list-view = Listvy
show-hidden-files = Visa dolda filer show-hidden-files = Visa dolda filer
list-directories-first = Lista mappar först list-directories-first = Lista mappar först
gallery-preview = Galleri förhandsvisning gallery-preview = Galleri förhandsvisning
menu-settings = Inställningar… menu-settings = Inställningar...
menu-about = Om COSMIC Filer... menu-about = Om COSMIC Filer...
## Sortera ## Sortera
@ -419,3 +419,9 @@ context-action-confirm-warning =
}. }.
run = Kör run = Kör
rename-confirm = Byt namn rename-confirm = Byt namn
checksum = { $kind }-kontrollsumma
calculate = Beräkna
error = Fel
sidebar-label = Sidofältsetikett
search-application = Sök på programnamn
change-sidebar-label = Ändra sidofältsetikett

View file

@ -202,17 +202,9 @@ moved =
*[other] ไฟล์ *[other] ไฟล์
} จาก "{ $from }" สู่ "{ $to }" } จาก "{ $from }" สู่ "{ $to }"
renaming = กำลังเปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }" renaming = กำลังเปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }"
renamed = Renamed "{ $from }" to "{ $to }" renamed = เปลี่ยนชื่อจาก "{ $from }" เป็น "{ $to }" แล้ว
restoring = restoring = กำลังกู้คืน { $items } รายการจาก { trash } ({ $progress })...
Restoring { $items } { $items -> restored = กู้คืน { $items } รายการจาก { trash } แล้ว
[one] item
*[other] items
} from { trash } ({ $progress })...
restored =
Restored { $items } { $items ->
[one] item
*[other] items
} from { trash }
unknown-folder = แฟ้มที่ไม่รู้จัก unknown-folder = แฟ้มที่ไม่รู้จัก
## Open with ## Open with
@ -240,8 +232,8 @@ settings = การตั้งค่า
appearance = ลักษณะ appearance = ลักษณะ
theme = ธีม theme = ธีม
match-desktop = ใช้ตามธีมหน้าจอหลัก match-desktop = ใช้ตามธีมหน้าจอหลัก
dark = ธีมมืด dark = มืด
light = ธีมสว่าง light = สว่าง
# Context menu # Context menu
add-to-sidebar = เพิ่มเข้าแถบด้านข้าง add-to-sidebar = เพิ่มเข้าแถบด้านข้าง
compress = บีบอัด compress = บีบอัด
@ -306,3 +298,4 @@ sort-newest-first = ไฟล์ใหม่ก่อน
sort-oldest-first = ไฟล์เก่าก่อน sort-oldest-first = ไฟล์เก่าก่อน
sort-smallest-to-largest = ขนาดเล็กก่อน sort-smallest-to-largest = ขนาดเล็กก่อน
sort-largest-to-smallest = ขนาดใหญ่ก่อน sort-largest-to-smallest = ขนาดใหญ่ก่อน
remove = ลบ

View file

@ -158,7 +158,7 @@ new-tab = Нова вкладка
new-window = Нове вікно new-window = Нове вікно
rename = Перейменувати... rename = Перейменувати...
close-tab = Закрити вкладку close-tab = Закрити вкладку
quit = Вийти quit = Завершити
## Edit ## Edit
@ -182,7 +182,7 @@ menu-settings = Налаштування...
menu-about = Про Файли COSMIC... menu-about = Про Файли COSMIC...
repository = Сховище repository = Сховище
support = Підтримка support = Підтримка
details = Деталі details = Подробиці
dismiss = Закрити повідомлення dismiss = Закрити повідомлення
remove = Вилучити remove = Вилучити
cancelled = Скасовані cancelled = Скасовані
@ -389,3 +389,9 @@ context-action-confirm-warning =
context-action = Контекстна дія context-action = Контекстна дія
rename-confirm = Перейменувати rename-confirm = Перейменувати
mixed = Змішано mixed = Змішано
checksum = контрольна сума { $kind }
calculate = Вирахувати
error = Помилка
sidebar-label = Назва бічної панелі
search-application = Пошук за назвою застосунку
change-sidebar-label = Змінити назву бічної панелі

View file

@ -25,7 +25,7 @@ modified = 修改时间
trashed-on = 删除时间 trashed-on = 删除时间
size = 大小 size = 大小
# Progress footer # Progress footer
details = 详细信息 details = 详情
dismiss = 清除消息 dismiss = 清除消息
operations-running = operations-running =
正在进行 { $running } { $running -> 正在进行 { $running } { $running ->
@ -169,8 +169,8 @@ read-write-execute = 读取、写入和执行
favorite-path-error = 打开目录时出错 favorite-path-error = 打开目录时出错
favorite-path-error-description = favorite-path-error-description =
无法打开 "{ $path }" 。 无法打开“{ $path }” 。
"{ $path }" 可能不存在或您没有权限打开它。 “{ $path }”可能不存在或您没有权限打开它。
您想要从侧边栏中移除它吗? 您想要从侧边栏中移除它吗?
remove = 移除 remove = 移除
@ -267,10 +267,10 @@ extracted =
[one] 项目 [one] 项目
*[other] 项目 *[other] 项目
}到“{ $to }” }到“{ $to }”
setting-executable-and-launching = 设置 "{ $name }" 为可执行文件并启动 setting-executable-and-launching = 设置 “{ $name }” 为可执行文件并启动
set-executable-and-launched = 已设 "{ $name }" 为可执行文件并且启动 set-executable-and-launched = 已设 “{ $name }” 为可执行文件并且启动
setting-permissions = 正在设置 "{ $name }" 的权限为 { $mode } setting-permissions = 正在设置 “{ $name }” 的权限为 { $mode }
set-permissions = 设置 "{ $name }" 的权限为 { $mode } set-permissions = 设置 “{ $name }” 的权限为 { $mode }
moving = moving =
正在从“{ $from }”移动 { $items } { $items -> 正在从“{ $from }”移动 { $items } { $items ->
[one] 项目 [one] 项目
@ -302,7 +302,7 @@ removed-from-recents =
*[other] 项目 *[other] 项目
} }
renaming = 正在将“{ $from }”重命名为“{ $to }” renaming = 正在将“{ $from }”重命名为“{ $to }”
renamed = 已重命名"{ $from }"为"{ $to }" renamed = 已重命名 “{ $from }” 为 “{ $to }”
restoring = restoring =
正在从{ trash }中还原 { $items } { $items -> 正在从{ trash }中还原 { $items } { $items ->
[one] 项目 [one] 项目
@ -446,3 +446,9 @@ context-action-confirm-warning =
} 上运行。 } 上运行。
context-action = 环境行动 context-action = 环境行动
rename-confirm = 重命名 rename-confirm = 重命名
calculate = 计算
error = 错误
checksum = { $kind } 校验和
sidebar-label = 侧边栏标签
search-application = 使用应用名称搜索
change-sidebar-label = 更改侧边栏标签

View file

@ -12,7 +12,7 @@ undo = 復原
today = 今天 today = 今天
# List view # List view
name = 名稱 name = 名稱
modified = 修改日期 modified = 修改時間
size = 大小 size = 大小
# Dialogs # Dialogs
@ -42,7 +42,7 @@ name-no-slashes = 名稱不能包含斜線
## Open/Save Dialog ## Open/Save Dialog
cancel = 取消 cancel = 取消
create = 建立 create = 創作
open = 開啟 open = 開啟
open-file = 開啟檔案 open-file = 開啟檔案
open-folder = 開啟資料夾 open-folder = 開啟資料夾
@ -161,8 +161,8 @@ moved =
[one] 項目 [one] 項目
*[other] 項目 *[other] 項目
} 從「{ $from }」至「{ $to }」 } 從「{ $from }」至「{ $to }」
renaming = 正在重新命名「{ $from }」至「{ $to }」 renaming = 正在將「{ $from }」重新命名為「{ $to }」
renamed = 已經重新命名「{ $from }」至「{ $to }」 renamed = 已將「{ $from }」重新命名為「{ $to }」
restoring = restoring =
正在還原 { $items } 項目 { $items -> 正在還原 { $items } 項目 { $items ->
[one] 項目 [one] 項目
@ -182,7 +182,7 @@ default-app = { $name } (預設)
## Show details ## Show details
show-details = 顯示詳細資料 show-details = 顯示詳細資訊
## Settings ## Settings
@ -196,13 +196,13 @@ match-desktop = 符合桌面
dark = 深色 dark = 深色
light = 淺色 light = 淺色
# Context menu # Context menu
add-to-sidebar = 添加至側邊欄 add-to-sidebar = 加入側邊欄
compress = 壓縮… compress = 壓縮…
extract-here = 解壓縮 extract-here = 解壓縮
new-file = 新建檔案... new-file = 新增檔案...
new-folder = 新建資料夾... new-folder = 新增資料夾...
open-in-terminal = 在終端機中開啟 open-in-terminal = 在終端機中開啟
move-to-trash = 移動至垃圾桶 move-to-trash = 丟入垃圾桶
restore-from-trash = 從垃圾桶還原 restore-from-trash = 從垃圾桶還原
remove-from-sidebar = 從側邊欄移除 remove-from-sidebar = 從側邊欄移除
sort-by-name = 依名稱排序 sort-by-name = 依名稱排序
@ -215,11 +215,11 @@ sort-by-size = 依大小排序
## File ## File
file = 檔案 file = 檔案
new-tab = 新建分頁 new-tab = 新增分頁
new-window = 新建視窗 new-window = 新增視窗
rename = 重新命名... rename = 重新命名...
close-tab = 關閉分頁 close-tab = 關閉分頁
quit = 退出 quit = 結束
## Edit ## Edit
@ -258,18 +258,18 @@ deleted =
}從 { trash } }從 { trash }
permanently-deleting = permanently-deleting =
正在永久刪除 { $items } { $items -> 正在永久刪除 { $items } { $items ->
[one] 项目 [one] 項目
*[other] 项目 *[other] 項目
} }
permanently-deleted = permanently-deleted =
已經永久刪除 { $items } { $items -> 已經永久刪除 { $items } { $items ->
[one] 项目 [one] 項目
*[other] 项目 *[other] 項目
} }
removing-from-recents = removing-from-recents =
正在從 { recents } 中移除 { $items } { $items -> 正在從 { recents } 中移除 { $items } { $items ->
[one] 项目 [one] 項目
*[other] 项目 *[other] 項目
} }
deleting = deleting =
正在刪除 { $items } { $items -> 正在刪除 { $items } { $items ->
@ -278,21 +278,21 @@ deleting =
}從 { trash }({ $progress })… }從 { trash }({ $progress })…
removed-from-recents = removed-from-recents =
已經從 { recents } 中移除 { $items } { $items -> 已經從 { recents } 中移除 { $items } { $items ->
[one] 项目 [one] 項目
*[other] 项目 *[other] 項目
} }
repository = 軟體庫源 repository = 軟體庫
desktop-view-options = 桌面檢視選項... desktop-view-options = 桌面檢視選項...
show-on-desktop = 顯示在桌面 show-on-desktop = 顯示在桌面
desktop-folder-content = 桌面資料夾內容 desktop-folder-content = 桌面資料夾內容
mounted-drives = 已經掛載的磁碟機 mounted-drives = 已經掛載的磁碟機
trash-folder-icon = 垃圾桶圖示 trash-folder-icon = 垃圾桶圖示
trashed-on = 遺棄時間 trashed-on = 丟入時間
icon-size-and-spacing = 圖示大小與間距 icon-size-and-spacing = 圖示大小與間距
icon-size = 圖示大小 icon-size = 圖示大小
grid-spacing = 網格間距 grid-spacing = 網格間距
details = 詳情 details = 詳細資訊
dismiss = 撤停訊息 dismiss = 關閉訊息
delete = 刪除 delete = 刪除
remove = 移除 remove = 移除
support = 支援 support = 支援
@ -393,3 +393,9 @@ context-action-confirm-warning =
} 上執行。 } 上執行。
run = 執行 run = 執行
rename-confirm = 重新命名 rename-confirm = 重新命名
checksum = { $kind } 核對和
calculate = 計算
error = 錯誤
sidebar-label = 側邊欄標籤
search-application = 依應用程式名稱搜尋
change-sidebar-label = 變更側邊欄標籤

View file

@ -12,6 +12,10 @@ cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
bin-src := cargo-target-dir / 'release' / name bin-src := cargo-target-dir / 'release' / name
bin-dst := base-dir / 'bin' / name bin-dst := base-dir / 'bin' / name
thumbnailer-bin-name := name + '-thumbnailer'
thumbnailer-bin-src := cargo-target-dir / 'release' / thumbnailer-bin-name
thumbnailer-bin-dst := base-dir / 'bin' / thumbnailer-bin-name
applet-name := name + '-applet' applet-name := name + '-applet'
applet-src := cargo-target-dir / 'release' / applet-name applet-src := cargo-target-dir / 'release' / applet-name
applet-dst := base-dir / 'bin' / applet-name applet-dst := base-dir / 'bin' / applet-name
@ -24,6 +28,10 @@ metainfo := APPID + '.metainfo.xml'
metainfo-src := 'target/xdgen' / metainfo metainfo-src := 'target/xdgen' / metainfo
metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo
thumbnailer := APPID + '.thumbnailer'
thumbnailer-src := 'res' / thumbnailer
thumbnailer-dst := clean(rootdir / prefix) / 'share' / 'thumbnailers' / thumbnailer
icons-src := 'res' / 'icons' / 'hicolor' icons-src := 'res' / 'icons' / 'hicolor'
icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor' icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor'
@ -45,14 +53,11 @@ clean-dist: clean clean-vendor
build-debug *args: build-debug *args:
cargo build {{args}} cargo build {{args}}
cargo build --package {{applet-name}} {{args}} cargo build --package {{applet-name}} {{args}}
cargo build --package {{thumbnailer-bin-name}} {{args}}
# Compiles with release profile # Compiles with release profile
build-release *args: (build-debug '--release' args) build-release *args: (build-debug '--release' args)
# Compiles applet with release profile
build-release-applet *args:
cargo build --package {{applet-name}} --release {{args}}
# Compiles release profile with vendored dependencies # Compiles release profile with vendored dependencies
build-vendored *args: vendor-extract (build-release '--frozen --offline' args) build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
@ -92,9 +97,11 @@ heaptrack *args:
# Installs files # Installs files
install: install:
install -Dm0755 {{bin-src}} {{bin-dst}} install -Dm0755 {{bin-src}} {{bin-dst}}
install -Dm0755 {{thumbnailer-bin-src}} {{thumbnailer-bin-dst}}
install -Dm0755 {{applet-src}} {{applet-dst}} install -Dm0755 {{applet-src}} {{applet-dst}}
install -Dm0644 {{desktop-src}} {{desktop-dst}} install -Dm0644 {{desktop-src}} {{desktop-dst}}
install -Dm0644 {{metainfo-src}} {{metainfo-dst}} install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
install -Dm0644 {{thumbnailer-src}} {{thumbnailer-dst}}
for size in `ls {{icons-src}}`; do \ for size in `ls {{icons-src}}`; do \
install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \ install -Dm0644 "{{icons-src}}/$size/apps/{{APPID}}.svg" "{{icons-dst}}/$size/apps/{{APPID}}.svg"; \
done done
@ -105,7 +112,7 @@ install-applet:
# Uninstalls installed files # Uninstalls installed files
uninstall: uninstall:
rm -f {{bin-dst}} {{applet-dst}} rm -f {{bin-dst}} {{thumbnailer-bin-dst}} {{applet-dst}} {{thumbnailer-dst}}
# Vendor dependencies locally # Vendor dependencies locally
vendor: vendor:

View file

@ -0,0 +1,4 @@
[Thumbnailer Entry]
TryExec=cosmic-files-thumbnailer
Exec=cosmic-files-thumbnailer %o --size %s %i
MimeType=application/vnd.microsoft.portable-executable;application/x-msdownload;

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::app::{self, Core, Task, context_drawer}; use cosmic::app::{self, Core, Task, context_drawer};
use cosmic::core::Auto;
use cosmic::cosmic_config::{self, ConfigSet}; use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::iced::clipboard::dnd::DndAction; use cosmic::iced::clipboard::dnd::DndAction;
use cosmic::iced::core::SmolStr; use cosmic::iced::core::SmolStr;
@ -16,18 +17,16 @@ use cosmic::iced::widget::button::focus;
use cosmic::iced::widget::scrollable; use cosmic::iced::widget::scrollable;
use cosmic::iced::widget::scrollable::AbsoluteOffset; use cosmic::iced::widget::scrollable::AbsoluteOffset;
use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId}; use cosmic::iced::window::{self, Event as WindowEvent, Id as WindowId};
use cosmic::iced::{ use cosmic::iced::{self, Alignment, Event, Length, Size, Subscription, event, mouse, stream};
self, Alignment, Event, Length, Rectangle, Size, Subscription, event, mouse, stream,
};
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
use cosmic::iced::{ use cosmic::iced::{
Limits, Point, Limits, Point, Rectangle,
event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent},
platform_specific::runtime::wayland::layer_surface::{ platform_specific::runtime::wayland::layer_surface::{
IcedMargin, IcedOutput, SctkLayerSurfaceSettings, IcedMargin, IcedOutput, SctkLayerSurfaceSettings,
}, },
platform_specific::shell::wayland::commands::layer_surface::{ platform_specific::shell::wayland::commands::layer_surface::{
Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, get_layer_surface, Anchor, KeyboardInteractivity, Layer, destroy_layer_surface,
}, },
}; };
use cosmic::widget::about::About; use cosmic::widget::about::About;
@ -36,7 +35,7 @@ use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::segmented_button::{self, Entity, ReorderEvent}; use cosmic::widget::segmented_button::{self, Entity, ReorderEvent};
use cosmic::widget::{self, icon, settings, space}; use cosmic::widget::{self, icon, settings, space};
use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, style, surface, theme}; use cosmic::{Application, ApplicationExt, Element, cosmic_theme, executor, surface, theme};
use mime_guess::Mime; use mime_guess::Mime;
use notify_debouncer_full::notify::{self, RecommendedWatcher}; use notify_debouncer_full::notify::{self, RecommendedWatcher};
use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
@ -427,6 +426,7 @@ pub enum NavMenuAction {
Preview(segmented_button::Entity), Preview(segmented_button::Entity),
RunContextAction(segmented_button::Entity, usize), RunContextAction(segmented_button::Entity, usize),
RemoveFromSidebar(segmented_button::Entity), RemoveFromSidebar(segmented_button::Entity),
ChangeSidebarLabel(segmented_button::Entity),
} }
impl MenuAction for NavMenuAction { impl MenuAction for NavMenuAction {
@ -502,6 +502,7 @@ pub enum Message {
OpenWithBrowse, OpenWithBrowse,
OpenWithDialog(Option<Entity>), OpenWithDialog(Option<Entity>),
OpenWithSelection(usize), OpenWithSelection(usize),
OpenWithSearchClear,
OpenWithToggleDefault(bool), OpenWithToggleDefault(bool),
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Overlap(window::Id, OverlapNotifyEvent), Overlap(window::Id, OverlapNotifyEvent),
@ -605,7 +606,7 @@ pub enum Message {
OutputEvent(OutputEvent, WlOutput), OutputEvent(OutputEvent, WlOutput),
Cosmic(app::Action), Cosmic(app::Action),
None, None,
Surface(surface::Action), Surface(surface::Action<Message>),
CutPaths(Vec<PathBuf>), CutPaths(Vec<PathBuf>),
} }
@ -690,6 +691,7 @@ pub enum DialogPage {
mime: mime_guess::Mime, mime: mime_guess::Mime,
selected: usize, selected: usize,
store_opt: Option<Arc<MimeApp>>, store_opt: Option<Arc<MimeApp>>,
search_app_name: String,
/// When true, the chosen app is written to mimeapps.list as the /// When true, the chosen app is written to mimeapps.list as the
/// default handler for `mime` after it spawns. /// default handler for `mime` after it spawns.
set_default: bool, set_default: bool,
@ -700,6 +702,10 @@ pub enum DialogPage {
DeleteTrash { DeleteTrash {
items: Vec<TrashItem>, items: Vec<TrashItem>,
}, },
ChangeSidebarLabel {
entity: Entity,
label: String,
},
RenameItem { RenameItem {
from: PathBuf, from: PathBuf,
parent: PathBuf, parent: PathBuf,
@ -792,7 +798,6 @@ pub struct MounterData(MounterKey, MounterItem);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum WindowKind { pub enum WindowKind {
ContextMenu(Entity, widget::Id),
Desktop(Entity), Desktop(Entity),
DesktopViewOptions, DesktopViewOptions,
Dialogs(widget::Id), Dialogs(widget::Id),
@ -1585,12 +1590,6 @@ impl App {
fn remove_window(&mut self, id: &window::Id) { fn remove_window(&mut self, id: &window::Id) {
if let Some(window) = self.windows.remove(id) { if let Some(window) = self.windows.remove(id) {
match window.kind { match window.kind {
WindowKind::ContextMenu(entity, _) => {
// Close context menu
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
tab.context_menu = None;
}
}
WindowKind::Desktop(entity) => { WindowKind::Desktop(entity) => {
// Remove the tab from the tab model // Remove the tab from the tab model
self.tab_model.remove(entity); self.tab_model.remove(entity);
@ -1927,21 +1926,6 @@ impl App {
} }
} }
fn close_context_menus(&mut self) -> Task<Message> {
let active = self.tab_model.active();
if let Some(tab) = self.tab_model.data_mut::<Tab>(active) {
tab.location_context_menu_index = None;
if tab.context_menu.is_some() {
return self.update(Message::TabMessage(
Some(active),
tab::Message::ContextMenu(None, None),
));
}
}
Task::none()
}
fn update_nav_model(&mut self) { fn update_nav_model(&mut self) {
let mut nav_model = segmented_button::ModelBuilder::default(); let mut nav_model = segmented_button::ModelBuilder::default();
@ -1955,15 +1939,7 @@ impl App {
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() { for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
if let Some(path) = favorite.path_opt() { if let Some(path) = favorite.path_opt() {
let name = if matches!(favorite, Favorite::Home) { let name = favorite.display_name().unwrap_or_else(|| fl!("filesystem"));
fl!("home")
} else if let Favorite::Network { name, .. } = favorite {
name.clone()
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
file_name.to_string()
} else {
fl!("filesystem")
};
nav_model = nav_model.insert(move |b| { nav_model = nav_model.insert(move |b| {
b.text(name.clone()) b.text(name.clone())
.icon( .icon(
@ -2600,15 +2576,33 @@ impl App {
.favorites .favorites
.iter() .iter()
.map(|favorite| { .map(|favorite| {
if let Favorite::Path(path) = favorite { match favorite {
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) { Favorite::Path(path) => {
if path.starts_with(from) for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
&& let Ok(relative) = path.strip_prefix(from)
{ {
favorites_changed = true; if path.starts_with(from)
return Favorite::from_path(to.join(relative)); && let Ok(relative) = path.strip_prefix(from)
{
favorites_changed = true;
return Favorite::from_path(to.join(relative));
}
} }
} }
Favorite::Named { path, name } => {
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
{
if path.starts_with(from)
&& let Ok(relative) = path.strip_prefix(from)
{
favorites_changed = true;
return Favorite::Named {
path: to.join(relative),
name: name.clone(),
};
}
}
}
_ => {}
} }
favorite.clone() favorite.clone()
}) })
@ -2718,6 +2712,9 @@ impl Application for App {
), ),
]); ]);
if matches!(flags.mode, Mode::Desktop) {
core.set_auto_blur(Auto::Window | Auto::Popup);
}
let mut app = Self { let mut app = Self {
core, core,
about, about,
@ -2836,8 +2833,16 @@ impl Application for App {
cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity))) cosmic::Action::App(Message::NavMenuAction(NavMenuAction::OpenInNewTab(entity)))
}) })
.context_menu(self.nav_context_menu()) .context_menu(self.nav_context_menu())
.close_icon(icon::from_name("media-eject-symbolic").size(16).icon()) .close_icon(icon::from_name("media-eject-symbolic").size(16).icon());
.into_container();
#[cfg(feature = "wayland")]
{
nav = nav
.window_id_maybe(self.core().main_window_id())
.on_surface_action(|action| cosmic::Action::Surface(action.flatten()))
}
let mut nav = nav.into_container();
if !self.core.is_condensed() { if !self.core.is_condensed() {
nav = nav.max_width(280); nav = nav.max_width(280);
@ -2848,14 +2853,12 @@ impl Application for App {
)) ))
} }
fn nav_context_menu( fn nav_context_menu(&self) -> Option<Vec<widget::menu::Tree<cosmic::Action<Self::Message>>>> {
&self,
) -> Option<Vec<widget::menu::Tree<cosmic::Action<Self::Message>>>> {
let items = self.nav_model.iter().map(|entity| { let items = self.nav_model.iter().map(|entity| {
let favorite_index_opt = self.nav_model.data::<FavoriteIndex>(entity); let favorite_index_opt = self.nav_model.data::<FavoriteIndex>(entity);
let location_opt = self.nav_model.data::<Location>(entity); let location_opt = self.nav_model.data::<Location>(entity);
let mut items = Vec::with_capacity(7); let mut items: Vec<widget::menu::Item<NavMenuAction, String>> = Vec::with_capacity(7);
if location_opt if location_opt
.and_then(Location::path_opt) .and_then(Location::path_opt)
@ -2915,6 +2918,11 @@ impl Application for App {
} }
items.push(cosmic::widget::menu::Item::Divider); items.push(cosmic::widget::menu::Item::Divider);
if favorite_index_opt.is_some() { if favorite_index_opt.is_some() {
items.push(cosmic::widget::menu::Item::Button(
fl!("change-sidebar-label"),
None,
NavMenuAction::ChangeSidebarLabel(entity),
));
items.push(cosmic::widget::menu::Item::Button( items.push(cosmic::widget::menu::Item::Button(
fl!("remove-from-sidebar"), fl!("remove-from-sidebar"),
None, None,
@ -2982,10 +2990,14 @@ impl Application for App {
}) })
&& let Some(mounter) = MOUNTERS.get(&key) && let Some(mounter) = MOUNTERS.get(&key)
{ {
return mounter.network_drive(uri.clone()).map(move |()| { return mounter.network_drive(uri.clone()).map(move |mounted| {
cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount { if mounted {
entity, cosmic::Action::App(Message::NetworkDriveOpenEntityAfterMount {
}) entity,
})
} else {
cosmic::action::none()
}
}); });
} }
@ -3095,18 +3107,6 @@ impl Application for App {
return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false))); return cosmic::task::message(cosmic::action::app(Message::SetShowDetails(false)));
} }
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
if tab.location_context_menu_index.is_some() {
tab.location_context_menu_index = None;
return Task::none();
}
if tab.context_menu.is_some() {
return self.update(Message::TabMessage(
Some(entity),
tab::Message::ContextMenu(None, None),
));
}
if tab.edit_location.is_some() { if tab.edit_location.is_some() {
tab.edit_location = None; tab.edit_location = None;
return Task::none(); return Task::none();
@ -3202,7 +3202,8 @@ impl Application for App {
} else { } else {
Favorite::from_path(path) Favorite::from_path(path)
}; };
if !favorites.contains(&favorite) { let favorite_path = favorite.path_opt();
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
favorites.push(favorite); favorites.push(favorite);
} }
} }
@ -3509,10 +3510,18 @@ impl Application for App {
path, path,
mime, mime,
selected, selected,
search_app_name,
set_default, set_default,
.. ..
} => { } => {
let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true); let mut available_apps =
self.mime_app_cache.get_apps_for_mime(&mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
if let Some((app, _)) = available_apps.get(selected) { if let Some((app, _)) = available_apps.get(selected) {
if let Some(mut command) = if let Some(mut command) =
@ -3558,6 +3567,18 @@ impl Application for App {
DialogPage::DeleteTrash { items } => { DialogPage::DeleteTrash { items } => {
tasks.push(self.operation(Operation::DeleteTrash { items })); tasks.push(self.operation(Operation::DeleteTrash { items }));
} }
DialogPage::ChangeSidebarLabel { entity, label } => {
if let Some(FavoriteIndex(favorite_i)) =
self.nav_model.data::<FavoriteIndex>(entity)
{
let mut favorites = self.config.favorites.clone();
if let Some(favorite) = favorites.get_mut(*favorite_i) {
*favorite = favorite.with_label(label.trim());
config_set!(favorites, favorites);
tasks.push(self.update_config());
}
}
}
DialogPage::RenameItem { DialogPage::RenameItem {
from, parent, name, .. from, parent, name, ..
} => { } => {
@ -3843,7 +3864,7 @@ impl Application for App {
Message::Mouse(window_id, _button) => { Message::Mouse(window_id, _button) => {
// Close context menu when clicking outside. // Close context menu when clicking outside.
if self.core.main_window_id() == Some(window_id) { if self.core.main_window_id() == Some(window_id) {
return self.close_context_menus(); return Task::none();
} }
} }
Message::MoveTo(entity_opt) => { Message::MoveTo(entity_opt) => {
@ -3896,7 +3917,7 @@ impl Application for App {
Some((*mounter_key, self.network_drive_input.clone())); Some((*mounter_key, self.network_drive_input.clone()));
return mounter return mounter
.network_drive(self.network_drive_input.clone()) .network_drive(self.network_drive_input.clone())
.map(|()| cosmic::action::none()); .map(|_| cosmic::action::none());
} }
log::warn!( log::warn!(
"no mounter found for connecting to {:?}", "no mounter found for connecting to {:?}",
@ -4159,21 +4180,25 @@ impl Application for App {
let Some(path) = item.path_opt() else { let Some(path) = item.path_opt() else {
continue; continue;
}; };
return self.push_dialog( return Task::batch([
DialogPage::OpenWith { self.push_dialog(
path: path.clone(), DialogPage::OpenWith {
mime: item.mime.clone(), path: path.clone(),
selected: 0, mime: item.mime.clone(),
store_opt: "x-scheme-handler/mime" selected: 0,
.parse::<mime_guess::Mime>() store_opt: "x-scheme-handler/mime"
.ok() .parse::<mime_guess::Mime>()
.and_then(|mime| { .ok()
self.mime_app_cache.get(&mime).first().cloned() .and_then(|mime| {
}), self.mime_app_cache.get(&mime).first().cloned()
set_default: false, }),
}, search_app_name: String::new(),
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), set_default: false,
); },
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
} }
} }
} }
@ -4189,6 +4214,14 @@ impl Application for App {
*set_default = enabled; *set_default = enabled;
} }
} }
Message::OpenWithSearchClear => {
if let Some(DialogPage::OpenWith {
search_app_name, ..
}) = self.dialog_pages.front_mut()
{
*search_app_name = String::new();
}
}
Message::Paste(entity_opt) => { Message::Paste(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
@ -4644,7 +4677,7 @@ impl Application for App {
)); ));
} }
Message::SearchActivate => { Message::SearchActivate => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
if self.search_get().is_none() { if self.search_get().is_none() {
tasks.push(self.search_set_active(Some(String::new()))); tasks.push(self.search_set_active(Some(String::new())));
@ -4655,7 +4688,7 @@ impl Application for App {
return Task::batch(tasks); return Task::batch(tasks);
} }
Message::SearchClear => { Message::SearchClear => {
return Task::batch([self.close_context_menus(), self.search_set_active(None)]); return self.search_set_active(None);
} }
Message::SearchInput(input) => { Message::SearchInput(input) => {
return self.search_set_active(Some(input)); return self.search_set_active(Some(input));
@ -4756,7 +4789,7 @@ impl Application for App {
return self.update_config(); return self.update_config();
} }
Message::TabActivate(entity) => { Message::TabActivate(entity) => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
// Activate new tab // Activate new tab
self.tab_model.activate(entity); self.tab_model.activate(entity);
@ -4818,7 +4851,9 @@ impl Application for App {
tasks.push(Task::future(async move { tasks.push(Task::future(async move {
cosmic::action::app(Message::WindowClose) cosmic::action::app(Message::WindowClose)
})); }));
} else if let Some(position) = self.tab_model.position(entity) { } else if entity == self.tab_model.active()
&& let Some(position) = self.tab_model.position(entity)
{
let new_position = if position > 0 { let new_position = if position > 0 {
position - 1 position - 1
} else { } else {
@ -4855,6 +4890,8 @@ impl Application for App {
} }
Message::TabMessage(entity_opt, tab_message) => { Message::TabMessage(entity_opt, tab_message) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
// The context menu opens on right-button release, so refresh paste availability now
let right_click = matches!(tab_message, tab::Message::RightClick(..));
let tab_commands = match self.tab_model.data_mut::<Tab>(entity) { let tab_commands = match self.tab_model.data_mut::<Tab>(entity) {
Some(tab) => tab.update(tab_message, self.modifiers), Some(tab) => tab.update(tab_message, self.modifiers),
@ -4862,6 +4899,9 @@ impl Application for App {
}; };
let mut commands = Vec::new(); let mut commands = Vec::new();
if right_click {
commands.push(self.update(Message::CheckClipboard));
}
for tab_command in tab_commands { for tab_command in tab_commands {
match tab_command { match tab_command {
tab::Command::Action(action) => { tab::Command::Action(action) => {
@ -4874,7 +4914,8 @@ impl Application for App {
tab::Command::AddToSidebar(path) => { tab::Command::AddToSidebar(path) => {
let mut favorites = self.config.favorites.clone(); let mut favorites = self.config.favorites.clone();
let favorite = Favorite::from_path(path); let favorite = Favorite::from_path(path);
if !favorites.contains(&favorite) { let favorite_path = favorite.path_opt();
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
favorites.push(favorite); favorites.push(favorite);
} }
config_set!(favorites, favorites); config_set!(favorites, favorites);
@ -4901,78 +4942,11 @@ impl Application for App {
self.update_tab(entity, tab_path, selection_paths), self.update_tab(entity, tab_path, selection_paths),
])); ]));
} }
tab::Command::ContextMenu(point_opt, parent_id) => { tab::Command::Surface(action) => {
#[cfg(feature = "wayland")] // re-type the tab's surface action the way its messages are re-typed
if let Some(point) = point_opt { let action = action
if crate::is_wayland() { .map(move |message| Message::TabMessage(Some(entity), message));
// Open context menu commands.push(self.update(Message::Surface(action)));
use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
Anchor, Gravity,
};
use cosmic::iced::runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner,
};
let window_id = WindowId::unique();
self.windows.insert(
window_id,
Window::new(WindowKind::ContextMenu(
entity,
widget::Id::unique(),
)),
);
commands.push(self.update(Message::CheckClipboard));
commands.push(self.update(Message::Surface(
cosmic::surface::action::app_popup(
|_: &Self| cosmic::surface::action::LiveSettings::default(),
move |app: &mut Self| -> SctkPopupSettings {
let anchor_rect = Rectangle {
x: point.x as i32,
y: point.y as i32,
width: 1,
height: 1,
};
let positioner = SctkPositioner {
size: None,
anchor_rect,
anchor: Anchor::None,
gravity: Gravity::BottomRight,
reactive: true,
..Default::default()
};
SctkPopupSettings {
parent: parent_id.unwrap_or(
app.core
.main_window_id()
.unwrap_or(WindowId::NONE),
),
id: window_id,
positioner,
parent_size: None,
grab: true,
close_with_children: false,
input_zone: None,
}
},
None,
),
)));
}
} else {
// Destroy previous popup
let mut window_ids = Vec::new();
for (window_id, window) in &self.windows {
if let WindowKind::ContextMenu(e, _) = &window.kind
&& *e == entity
{
window_ids.push(*window_id);
}
}
for window_id in window_ids {
commands.push(self.update(Message::Surface(
cosmic::surface::action::destroy_popup(window_id),
)));
}
}
} }
tab::Command::Delete(paths) => commands.push(self.delete(paths)), tab::Command::Delete(paths) => commands.push(self.delete(paths)),
tab::Command::DropFiles(to, from) => { tab::Command::DropFiles(to, from) => {
@ -5022,7 +4996,6 @@ impl Application for App {
} }
tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)), tab::Command::OpenFile(paths) => commands.push(self.open_file(&paths)),
tab::Command::OpenInNewTab(path) => { tab::Command::OpenInNewTab(path) => {
commands.push(self.close_context_menus());
commands.push(self.open_tab(Location::Path(path), false, None)); commands.push(self.open_tab(Location::Path(path), false, None));
} }
tab::Command::OpenInNewWindow(path) => match env::current_exe() { tab::Command::OpenInNewWindow(path) => match env::current_exe() {
@ -5459,12 +5432,6 @@ impl Application for App {
if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(tab_entity) {
// Close location editing if enabled // Close location editing if enabled
tab.edit_location = None; tab.edit_location = None;
// Close other context menus.
tab.location_context_menu_index = None;
return Task::done(cosmic::Action::App(Message::TabMessage(
Some(tab_entity),
tab::Message::ContextMenu(None, None),
)));
} }
} }
Message::NavMenuAction(action) => match action { Message::NavMenuAction(action) => match action {
@ -5508,6 +5475,7 @@ impl Application for App {
.and_then(|mime| { .and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned() self.mime_app_cache.get(&mime).first().cloned()
}), }),
search_app_name: String::new(),
set_default: false, set_default: false,
}, },
None, None,
@ -5562,7 +5530,7 @@ impl Application for App {
_ => Task::none(), _ => Task::none(),
}; };
return Task::batch([self.close_context_menus(), open_task]); return open_task;
} }
// Open the selected path in a new cosmic-files window. // Open the selected path in a new cosmic-files window.
@ -5643,6 +5611,20 @@ impl Application for App {
return self.update_config(); return self.update_config();
} }
} }
NavMenuAction::ChangeSidebarLabel(entity) => {
if let Some(favorite) = self.nav_model.data::<FavoriteIndex>(entity).and_then(
|FavoriteIndex(favorite_i)| self.config.favorites.get(*favorite_i),
) {
let label = favorite.display_name().unwrap_or_else(|| fl!("filesystem"));
return Task::batch([
self.dialog_pages
.push_back(DialogPage::ChangeSidebarLabel { entity, label }),
widget::text_input::focus(self.dialog_text_input.clone()),
widget::text_input::select_all(self.dialog_text_input.clone()),
]);
}
}
}, },
Message::Recents => { Message::Recents => {
if self.config.show_recents { if self.config.show_recents {
@ -5694,24 +5676,33 @@ impl Application for App {
.insert(surface_id, Window::new(WindowKind::Desktop(entity))); .insert(surface_id, Window::new(WindowKind::Desktop(entity)));
return Task::batch([ return Task::batch([
command, command,
get_layer_surface(SctkLayerSurfaceSettings { cosmic::task::message(cosmic::Action::Surface(
id: surface_id, cosmic::surface::action::app_layer_shell(
layer: Layer::Bottom, |_| Default::default(),
keyboard_interactivity: KeyboardInteractivity::OnDemand, move |_: &mut App| SctkLayerSurfaceSettings {
input_zone: None, id: surface_id,
anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, layer: Layer::Bottom,
output: IcedOutput::Output(output), keyboard_interactivity: KeyboardInteractivity::OnDemand,
namespace: "cosmic-files-applet".into(), input_zone: None,
size: Some((None, None)), anchor: Anchor::TOP
margin: IcedMargin { | Anchor::BOTTOM
top: 0, | Anchor::LEFT
bottom: 0, | Anchor::RIGHT,
left: 0, output: IcedOutput::Output(output.clone()),
right: 0, namespace: "cosmic-files-applet".into(),
}, size: Some((None, None)),
exclusive_zone: 0, margin: IcedMargin {
size_limits: Limits::NONE.min_width(1.0).min_height(1.0), top: 0,
}), bottom: 0,
left: 0,
right: 0,
},
exclusive_zone: 0,
size_limits: Limits::NONE.min_width(1.0).min_height(1.0),
},
None,
),
)),
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
overlap_notify(surface_id, true), overlap_notify(surface_id, true),
]); ]);
@ -5804,9 +5795,7 @@ impl Application for App {
}); });
} }
Message::Surface(action) => { Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic( return cosmic::task::message(cosmic::Action::Surface(action));
cosmic::app::Action::Surface(action),
));
} }
Message::SaveSortNames => { Message::SaveSortNames => {
self.must_save_sort_names = false; self.must_save_sort_names = false;
@ -6352,7 +6341,9 @@ impl Application for App {
mime, mime,
selected, selected,
store_opt, store_opt,
search_app_name,
set_default, set_default,
..
} => { } => {
let name = match path.file_name() { let name = match path.file_name() {
Some(file_name) => file_name.to_str(), Some(file_name) => file_name.to_str(),
@ -6360,7 +6351,13 @@ impl Application for App {
}; };
let mut column = widget::list_column(); let mut column = widget::list_column();
let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true); let mut available_apps = self.mime_app_cache.get_apps_for_mime(mime, true);
available_apps.retain(|(app, _)| {
app.name
.to_lowercase()
.trim()
.contains(search_app_name.to_lowercase().as_str().trim())
});
let item_height = 32.0; let item_height = 32.0;
let mut displayed_default = false; let mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact; let mut last_kind = MimeAppMatch::Exact;
@ -6424,6 +6421,25 @@ impl Application for App {
.secondary_action( .secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
) )
.control(
widget::text_input::search_input(
fl!("search-application"),
search_app_name,
)
.id(self.dialog_text_input.clone())
.on_clear(Message::OpenWithSearchClear)
.on_input(move |search_app_name| {
Message::DialogUpdate(DialogPage::OpenWith {
path: path.clone(),
mime: mime.clone(),
selected: *selected,
store_opt: store_opt.clone(),
search_app_name,
set_default: *set_default,
})
})
.on_submit(|_| Message::DialogComplete),
)
.control(widget::scrollable(column).height({ .control(widget::scrollable(column).height({
let max_size = self let max_size = self
.size .size
@ -6512,6 +6528,40 @@ impl Application for App {
target = target target = target
))) )))
} }
DialogPage::ChangeSidebarLabel { entity, label } => {
let entity = *entity;
let complete_maybe = if label.trim().is_empty() {
None
} else {
Some(Message::DialogComplete)
};
widget::dialog()
.title(fl!("change-sidebar-label"))
.primary_action(
widget::button::suggested(fl!("save"))
.on_press_maybe(complete_maybe.clone()),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
)
.control(
widget::column::with_children([
widget::text::body(fl!("sidebar-label")).into(),
widget::text_input("", label.as_str())
.id(self.dialog_text_input.clone())
.on_input(move |label| {
Message::DialogUpdate(DialogPage::ChangeSidebarLabel {
entity,
label,
})
})
.on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
.into(),
])
.spacing(space_xxs),
)
}
DialogPage::RenameItem { DialogPage::RenameItem {
from, from,
parent, parent,
@ -6898,7 +6948,6 @@ impl Application for App {
}) })
.drag_id(self.tab_drag_id), .drag_id(self.tab_drag_id),
) )
.class(style::Container::Background)
.width(Length::Fill) .width(Length::Fill)
.padding([0, space_s]), .padding([0, space_s]),
); );
@ -6962,23 +7011,6 @@ impl Application for App {
fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> { fn view_window(&self, id: WindowId) -> Element<'_, Self::Message> {
let content = match self.windows.get(&id) { let content = match self.windows.get(&id) {
Some(window) => match &window.kind { Some(window) => match &window.kind {
WindowKind::ContextMenu(entity, id) => match self.tab_model.data::<Tab>(*entity) {
Some(tab) => {
return widget::autosize::autosize(
menu::context_menu(
tab,
&self.key_binds,
&window.modifiers,
self.clipboard_has_content(),
&self.config.context_actions,
)
.map(|x| Message::TabMessage(Some(*entity), x)),
id.clone(),
)
.into();
}
None => widget::text("Unknown tab ID").into(),
},
WindowKind::Desktop(entity) => { WindowKind::Desktop(entity) => {
let mut tab_column = widget::column::with_capacity(3); let mut tab_column = widget::column::with_capacity(3);
@ -7038,6 +7070,7 @@ impl Application for App {
return self.view_main().map(|message| match message { return self.view_main().map(|message| match message {
cosmic::Action::App(app) => app, cosmic::Action::App(app) => app,
cosmic::Action::Cosmic(cosmic) => Message::Cosmic(cosmic), cosmic::Action::Cosmic(cosmic) => Message::Cosmic(cosmic),
cosmic::Action::Surface(action) => Message::Surface(action),
cosmic::Action::None => Message::None, cosmic::Action::None => Message::None,
}); });
} }

View file

@ -70,6 +70,11 @@ pub enum Favorite {
name: String, name: String,
path: PathBuf, path: PathBuf,
}, },
/// A path with a custom name chosen by the user
Named {
path: PathBuf,
name: String,
},
} }
impl Favorite { impl Favorite {
@ -98,6 +103,38 @@ impl Favorite {
Self::Videos => dirs::video_dir(), Self::Videos => dirs::video_dir(),
Self::Path(path) => Some(path.clone()), Self::Path(path) => Some(path.clone()),
Self::Network { path, .. } => Some(path.clone()), Self::Network { path, .. } => Some(path.clone()),
Self::Named { path, .. } => Some(path.clone()),
}
}
/// Name shown in the sidebar, or `None` if the path has no usable file name
pub fn display_name(&self) -> Option<String> {
match self {
Self::Home => Some(crate::fl!("home")),
Self::Named { name, .. } | Self::Network { name, .. } => Some(name.clone()),
_ => self
.path_opt()?
.file_name()
.and_then(|x| x.to_str())
.map(ToString::to_string),
}
}
/// Return this favorite with a custom sidebar label chosen by the user
pub fn with_label(&self, name: &str) -> Self {
match self {
Self::Network { uri, path, .. } => Self::Network {
uri: uri.clone(),
name: name.to_string(),
path: path.clone(),
},
other => match other.path_opt() {
Some(path) => Self::Named {
path,
name: name.to_string(),
},
None => other.clone(),
},
} }
} }
} }
@ -113,6 +150,7 @@ pub enum TypeToSearch {
#[serde(default)] #[serde(default)]
pub struct State { pub struct State {
pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>, pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>,
pub dialog_sort_name: (HeadingOptions, bool),
} }
impl Default for State { impl Default for State {
@ -124,6 +162,7 @@ impl Default for State {
(HeadingOptions::Modified, false), (HeadingOptions::Modified, false),
) )
})), })),
dialog_sort_name: (HeadingOptions::Modified, false),
} }
} }
} }
@ -476,3 +515,66 @@ pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime";
pub struct TimeConfig { pub struct TimeConfig {
pub military_time: bool, pub military_time: bool,
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn favorite_with_label_converts_path_to_named() {
let favorite = Favorite::Path(PathBuf::from("/some/dir"));
assert_eq!(
favorite.with_label("Custom"),
Favorite::Named {
path: PathBuf::from("/some/dir"),
name: "Custom".to_string(),
}
);
}
#[test]
fn favorite_with_label_updates_network_in_place() {
let favorite = Favorite::Network {
uri: "sftp://example.com/".to_string(),
name: "example.com".to_string(),
path: PathBuf::from("/run/mount/example"),
};
assert_eq!(
favorite.with_label("Custom"),
Favorite::Network {
uri: "sftp://example.com/".to_string(),
name: "Custom".to_string(),
path: PathBuf::from("/run/mount/example"),
}
);
}
#[test]
fn favorite_with_label_converts_special_folder_to_named() {
let home = dirs::home_dir().unwrap();
assert_eq!(
Favorite::Home.with_label("Custom"),
Favorite::Named {
path: home,
name: "Custom".to_string(),
}
);
}
#[test]
fn favorite_display_name() {
assert_eq!(
Favorite::Path(PathBuf::from("/some/dir")).display_name(),
Some("dir".to_string())
);
assert_eq!(Favorite::Path(PathBuf::from("/")).display_name(), None);
assert_eq!(
Favorite::Named {
path: PathBuf::from("/some/dir"),
name: "Custom".to_string(),
}
.display_name(),
Some("Custom".to_string())
);
}
}

View file

@ -3,6 +3,7 @@
use cosmic::app::cosmic::Cosmic; use cosmic::app::cosmic::Cosmic;
use cosmic::app::{Core, Task, context_drawer}; use cosmic::app::{Core, Task, context_drawer};
use cosmic::cosmic_config::ConfigSet;
use cosmic::iced::core::SmolStr; use cosmic::iced::core::SmolStr;
use cosmic::iced::core::widget::operation; use cosmic::iced::core::widget::operation;
use cosmic::iced::futures::{self, SinkExt}; use cosmic::iced::futures::{self, SinkExt};
@ -33,12 +34,12 @@ use crate::app::{
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID, Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
}; };
use crate::config::{ use crate::config::{
Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch, Config, DialogConfig, State, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
}; };
use crate::key_bind::key_binds; use crate::key_bind::key_binds;
use crate::localize::LANGUAGE_SORTER; use crate::localize::LANGUAGE_SORTER;
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage}; use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab}; use crate::tab::{self, HeadingOptions, ItemMetadata, Location, SearchLocation, Tab};
use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default}; use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
use crate::{fl, home_dir, menu, mime_icon}; use crate::{fl, home_dir, menu, mime_icon};
@ -253,6 +254,7 @@ impl<M: Send + 'static> Dialog<M> {
crate::localize::localize(); crate::localize::localize();
let (config_handler, config) = Config::load(); let (config_handler, config) = Config::load();
let (state_handler, state) = State::load();
let mut settings = window::Settings { let mut settings = window::Settings {
decorations: false, decorations: false,
@ -287,6 +289,8 @@ impl<M: Send + 'static> Dialog<M> {
window_id, window_id,
config_handler, config_handler,
config, config,
state_handler,
state,
}; };
let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags)); let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags));
@ -441,6 +445,8 @@ struct Flags {
#[allow(dead_code)] #[allow(dead_code)]
config_handler: Option<cosmic_config::Config>, config_handler: Option<cosmic_config::Config>,
config: Config, config: Config,
state_handler: Option<cosmic_config::Config>,
state: State,
} }
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
@ -470,7 +476,7 @@ enum Message {
SearchActivate, SearchActivate,
SearchClear, SearchClear,
SearchInput(String), SearchInput(String),
Surface(cosmic::surface::Action), Surface(cosmic::surface::Action<Message>),
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
TabMessage(tab::Message), TabMessage(tab::Message),
TabRescan( TabRescan(
@ -503,7 +509,7 @@ impl From<AppMessage> for Message {
AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn, AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn,
AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut, AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut,
AppMessage::NewItem(_entity_opt, true) => Self::NewFolder, AppMessage::NewItem(_entity_opt, true) => Self::NewFolder,
AppMessage::Surface(action) => Self::Surface(action), AppMessage::Surface(action) => Self::Surface(action.map(Self::from)),
unsupported => { unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode"); log::warn!("{unsupported:?} not supported in dialog mode");
Self::None Self::None
@ -543,7 +549,6 @@ struct App {
title: String, title: String,
accept_label: DialogLabel, accept_label: DialogLabel,
choices: Vec<DialogChoice>, choices: Vec<DialogChoice>,
context_menu_window: Option<window::Id>,
context_page: ContextPage, context_page: ContextPage,
dialog_pages: VecDeque<DialogPage>, dialog_pages: VecDeque<DialogPage>,
dialog_text_input: widget::Id, dialog_text_input: widget::Id,
@ -873,15 +878,6 @@ impl App {
} }
} }
fn close_context_menus(&mut self) -> Task<Message> {
self.tab.location_context_menu_index = None;
if self.tab.context_menu.is_some() {
return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
}
Task::none()
}
fn update_nav_model(&mut self) { fn update_nav_model(&mut self) {
let mut nav_model = segmented_button::ModelBuilder::default(); let mut nav_model = segmented_button::ModelBuilder::default();
@ -895,13 +891,7 @@ impl App {
for favorite in &self.flags.config.favorites { for favorite in &self.flags.config.favorites {
if let Some(path) = favorite.path_opt() { if let Some(path) = favorite.path_opt() {
let name = if matches!(favorite, Favorite::Home) { let Some(name) = favorite.display_name() else {
fl!("home")
} else if let Favorite::Network { name, .. } = favorite {
name.clone()
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
file_name.to_string()
} else {
continue; continue;
}; };
nav_model = nav_model.insert(move |b| { nav_model = nav_model.insert(move |b| {
@ -1051,8 +1041,7 @@ impl Application for App {
None, None,
); );
tab.mode = tab::Mode::Dialog(flags.kind.clone()); tab.mode = tab::Mode::Dialog(flags.kind.clone());
tab.sort_name = tab::HeadingOptions::Modified; (tab.sort_name, tab.sort_direction) = flags.state.dialog_sort_name;
tab.sort_direction = false;
let key_binds = key_binds(&tab.mode); let key_binds = key_binds(&tab.mode);
@ -1062,7 +1051,6 @@ impl Application for App {
title, title,
accept_label: DialogLabel::from(accept_label), accept_label: DialogLabel::from(accept_label),
choices: Vec::new(), choices: Vec::new(),
context_menu_window: None,
context_page: ContextPage::Preview(None, PreviewKind::Selected), context_page: ContextPage::Preview(None, PreviewKind::Selected),
dialog_pages: VecDeque::new(), dialog_pages: VecDeque::new(),
dialog_text_input: widget::Id::new("Dialog Text Input"), dialog_text_input: widget::Id::new("Dialog Text Input"),
@ -1333,15 +1321,6 @@ impl Application for App {
return Task::none(); return Task::none();
} }
if self.tab.location_context_menu_index.is_some() {
self.tab.location_context_menu_index = None;
return Task::none();
}
if self.tab.context_menu.is_some() {
return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
}
if self.tab.edit_location.is_some() { if self.tab.edit_location.is_some() {
// Close location editing if enabled // Close location editing if enabled
self.tab.edit_location = None; self.tab.edit_location = None;
@ -1576,7 +1555,7 @@ impl Application for App {
Message::Mouse(window_id, _button) => { Message::Mouse(window_id, _button) => {
// Close context menu when clicking outside. // Close context menu when clicking outside.
if self.core.main_window_id() == Some(window_id) { if self.core.main_window_id() == Some(window_id) {
return self.close_context_menus(); return Task::none();
} }
} }
Message::NewFolder => { Message::NewFolder => {
@ -1736,7 +1715,7 @@ impl Application for App {
))); )));
} }
Message::SearchActivate => { Message::SearchActivate => {
let mut tasks = vec![self.close_context_menus()]; let mut tasks = vec![];
if self.search_get().is_none() { if self.search_get().is_none() {
tasks.push(self.search_set(Some(String::new()))); tasks.push(self.search_set(Some(String::new())));
@ -1747,7 +1726,7 @@ impl Application for App {
return Task::batch(tasks); return Task::batch(tasks);
} }
Message::SearchClear => { Message::SearchClear => {
return Task::batch([self.close_context_menus(), self.search_set(None)]); return self.search_set(None);
} }
Message::SearchInput(input) => { Message::SearchInput(input) => {
return self.search_set(Some(input)); return self.search_set(Some(input));
@ -1783,78 +1762,9 @@ impl Application for App {
self.rescan_tab(selection_paths), self.rescan_tab(selection_paths),
])); ]));
} }
tab::Command::ContextMenu(point_opt, parent_id) => { tab::Command::Surface(action) => {
#[cfg(feature = "wayland")] let action = action.map(Message::TabMessage);
match point_opt { commands.push(self.update(Message::Surface(action)));
Some(point) => {
if crate::is_wayland() {
// Open context menu
use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
Anchor, Gravity,
};
use cosmic::iced::runtime::platform_specific::wayland::popup::{
SctkPopupSettings, SctkPositioner,
};
use cosmic::iced::Rectangle;
let window_id = window::Id::unique();
self.context_menu_window = Some(window_id);
let autosize_id = widget::Id::unique();
commands.push(self.update(Message::Surface(
cosmic::surface::action::app_popup(
|_: &Self| cosmic::surface::action::LiveSettings::default(),
move |app: &mut Self| -> SctkPopupSettings {
let anchor_rect = Rectangle {
x: point.x as i32,
y: point.y as i32,
width: 1,
height: 1,
};
let positioner = SctkPositioner {
size: None,
anchor_rect,
anchor: Anchor::None,
gravity: Gravity::BottomRight,
reactive: true,
..Default::default()
};
SctkPopupSettings {
parent: parent_id
.unwrap_or(app.flags.window_id),
id: window_id,
positioner,
parent_size: None,
grab: true,
close_with_children: false,
input_zone: None,
}
},
Some(Box::new(move |app: &Self| {
widget::autosize::autosize(
menu::context_menu(
&app.tab,
&app.key_binds,
&app.modifiers,
false, // Paste not used in dialogs
&app.flags.config.context_actions,
)
.map(Message::TabMessage)
.map(cosmic::Action::App),
autosize_id.clone(),
)
.into()
})),
),
)));
}
}
None => {
if let Some(window_id) = self.context_menu_window.take() {
commands.push(self.update(Message::Surface(
cosmic::surface::action::destroy_popup(window_id),
)));
}
}
}
} }
tab::Command::Iced(iced_command) => { tab::Command::Iced(iced_command) => {
commands.push(iced_command.0.map(|tab_message| { commands.push(iced_command.0.map(|tab_message| {
@ -1889,6 +1799,17 @@ impl Application for App {
self.auto_scroll_speed = None; self.auto_scroll_speed = None;
} }
} }
tab::Command::SetSort(_, heading_options, direction) => {
self.flags.state.dialog_sort_name = (heading_options, direction);
if let Some(state_handler) = self.flags.state_handler.as_ref()
&& let Err(err) = state_handler.set::<&(HeadingOptions, bool)>(
"dialog_sort_name",
&self.flags.state.dialog_sort_name,
)
{
log::warn!("Failed to save dialog sort name: {err:?}");
}
}
unsupported => { unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode"); log::warn!("{unsupported:?} not supported in dialog mode");
} }
@ -2009,9 +1930,7 @@ impl Application for App {
}); });
} }
Message::Surface(action) => { Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Cosmic( return cosmic::task::message(cosmic::Action::Surface(action));
cosmic::app::Action::Surface(action),
));
} }
} }

View file

@ -1,16 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::{Element, theme};
use cosmic::app::Core; use cosmic::app::Core;
use cosmic::iced::advanced::widget::text::Style as TextStyle;
use cosmic::iced::keyboard::Modifiers; use cosmic::iced::keyboard::Modifiers;
use cosmic::iced::{Alignment, Background, Border, Length}; use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar}; use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
use cosmic::widget::{ use cosmic::widget::{self, responsive_menu_bar};
self, Row, button, column, container, divider, responsive_menu_bar, space, text,
};
use cosmic::{Element, theme};
#[cfg(feature = "desktop")]
use i18n_embed::LanguageLoader; use i18n_embed::LanguageLoader;
use mime_guess::Mime; use mime_guess::Mime;
use std::collections::HashMap; use std::collections::HashMap;
@ -27,21 +23,6 @@ use crate::trash::{Trash, TrashExt};
static MENU_ID: LazyLock<cosmic::widget::Id> = static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
macro_rules! menu_button {
($($x:expr),+ $(,)?) => (
button::custom(
Row::with_children(
[$(Element::from($x)),+]
)
.height(Length::Fixed(24.0))
.align_y(Alignment::Center)
)
.padding([theme::spacing().space_xxs, 16])
.width(Length::Fill)
.class(theme::Button::MenuItem)
);
}
const fn menu_button_optional( const fn menu_button_optional(
label: String, label: String,
action: Action, action: Action,
@ -54,82 +35,46 @@ const fn menu_button_optional(
} }
} }
/// A menu action dispatched to the tab the menu was opened on.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TabAction(pub Action);
impl MenuAction for TabAction {
type Message = tab::Message;
fn message(&self) -> tab::Message {
tab::Message::ContextAction(self.0)
}
}
pub fn context_menu<'a>( pub fn context_menu<'a>(
tab: &Tab, tab: &Tab,
key_binds: &HashMap<KeyBind, Action>, key_binds: &HashMap<KeyBind, Action>,
modifiers: &Modifiers, modifiers: &Modifiers,
clipboard_paste_available: bool, clipboard_paste_available: bool,
context_actions: &[ContextActionPreset], context_actions: &[ContextActionPreset],
) -> Element<'a, tab::Message> { ) -> Vec<menu::Tree<tab::Message>> {
let find_key = |action: &Action| -> String { let menu_item =
for (key_bind, key_action) in key_binds { |label: String, action: Action| menu::Item::Button(label, None, TabAction(action));
if action == key_action { let menu_item_disabled =
return key_bind.to_string(); |label: String, action: Action| menu::Item::ButtonDisabled(label, None, TabAction(action));
}
}
String::new()
};
fn key_style(theme: &cosmic::Theme) -> TextStyle {
let mut color = theme.cosmic().background(false).component.on;
color.alpha *= 0.75;
TextStyle {
color: Some(color.into()),
..Default::default()
}
}
fn disabled_style(theme: &cosmic::Theme) -> TextStyle {
let mut color = theme.cosmic().background(false).component.on;
color.alpha *= 0.5;
TextStyle {
color: Some(color.into()),
..Default::default()
}
}
let menu_item = |label, action| {
let key = find_key(&action);
menu_button!(
text::body(label),
space::horizontal(),
text::body(key).class(theme::Text::Custom(key_style))
)
.on_press(tab::Message::ContextAction(action))
};
let menu_item_disabled = |label, action: Action| {
let key = find_key(&action);
menu_button!(
text::body(label).class(theme::Text::Custom(disabled_style)),
space::horizontal(),
text::body(key).class(theme::Text::Custom(disabled_style))
)
};
// Allow paste when clipboard has data and we're in a location that supports it // Allow paste when clipboard has data and we're in a location that supports it
let can_paste = clipboard_paste_available && tab.location.supports_paste(); let can_paste = clipboard_paste_available && tab.location.supports_paste();
let (sort_name, sort_direction, _) = tab.sort_options(); let (sort_name, sort_direction, _) = tab.sort_options();
let sort_item = |label, variant| { let sort_item = |label: String, variant| {
let key = find_key(&Action::ToggleSort(variant)); let entry = menu::Entry::new(label, TabAction(Action::ToggleSort(variant)));
let leading: Element<'a, tab::Message> = if sort_name == variant { menu::Item::Entry(if sort_name == variant {
let icon_name = if sort_direction { let icon_name = if sort_direction {
"view-sort-ascending-symbolic" "view-sort-ascending-symbolic"
} else { } else {
"view-sort-descending-symbolic" "view-sort-descending-symbolic"
}; };
widget::icon::from_name(icon_name).size(14).into() entry.icon(widget::icon::from_name(icon_name).size(14).handle())
} else { } else {
space::horizontal().width(Length::Fixed(14.0)).into() entry.reserve_icon()
}; })
menu_button!(
leading,
space::horizontal().width(Length::Fixed(theme::spacing().space_xxs.into())),
text::body(label),
space::horizontal(),
text::body(key).class(theme::Text::Custom(key_style))
)
.on_press(tab::Message::ContextAction(Action::ToggleSort(variant)))
.into()
}; };
let mut selected_dir = 0; let mut selected_dir = 0;
@ -174,8 +119,8 @@ pub fn context_menu<'a>(
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, action)| action.matches_selection(selected, selected_dir)) .filter(|(_, action)| action.matches_selection(selected, selected_dir))
.map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)).into()) .map(|(i, action)| menu_item(action.name.clone(), Action::RunContextAction(i)))
.collect::<Vec<Element<'a, tab::Message>>>() .collect::<Vec<_>>()
}; };
// Parse the desktop entry if it is the only selection // Parse the desktop entry if it is the only selection
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@ -190,7 +135,7 @@ pub fn context_menu<'a>(
} }
}); });
let mut children: Vec<Element<_>> = Vec::new(); let mut children: Vec<menu::Item<TabAction, String>> = Vec::new();
match (&tab.mode, &tab.location) { match (&tab.mode, &tab.location) {
( (
tab::Mode::App | tab::Mode::Desktop, tab::Mode::App | tab::Mode::Desktop,
@ -202,108 +147,114 @@ pub fn context_menu<'a>(
| Location::Network(_, _, Some(_)), | Location::Network(_, _, Some(_)),
) => { ) => {
if selected_trash_only { if selected_trash_only {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open));
if !Trash::is_empty() { if !Trash::is_empty() {
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash));
} }
} else if let Some(_entry) = selected_desktop_entry { } else if let Some(entry) = selected_desktop_entry {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open));
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
{ {
children.extend(_entry.desktop_actions.into_iter().enumerate().map( children.extend(
|(i, action)| menu_item(action.name, Action::ExecEntryAction(i)).into(), entry
)); .desktop_actions
.into_iter()
.enumerate()
.map(|(i, action)| menu_item(action.name, Action::ExecEntryAction(i))),
);
} }
children children.push(menu_item(fl!("edit-desktop-entry"), Action::EditDesktopEntry));
.push(menu_item(fl!("edit-desktop-entry"), Action::EditDesktopEntry).into()); children.push(menu::Item::Divider);
children.push(divider::horizontal::light().into()); children.push(menu_item(fl!("rename"), Action::Rename));
children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("cut"), Action::Cut));
children.push(menu_item(fl!("cut"), Action::Cut).into());
if modifiers.shift() && !modifiers.control() { if modifiers.shift() && !modifiers.control() {
children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); children.push(menu_item(fl!("copy-path"), Action::CopyPath));
} else { } else {
children.push(menu_item(fl!("copy"), Action::Copy).into()); children.push(menu_item(fl!("copy"), Action::Copy));
} }
// Should this simply bypass trash and remove the shortcut? // Should this simply bypass trash and remove the shortcut?
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); children.push(menu_item(fl!("move-to-trash"), Action::Delete));
let action_items = context_action_items(selected, selected_dir); let action_items = context_action_items(selected, selected_dir);
if !action_items.is_empty() { if !action_items.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.extend(action_items); children.extend(action_items);
} }
} else if selected > 0 { } else if selected > 0 {
if selected_dir == 1 && selected == 1 || selected_dir == 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open));
} }
if selected == 1 { if selected == 1 {
children.push(menu_item(fl!("menu-open-with"), Action::OpenWith).into()); children.push(menu_item(fl!("menu-open-with"), Action::OpenWith));
if selected_dir == 1 { if selected_dir == 1 {
children children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal));
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
} }
} }
if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) { if tab.location.is_recents() || matches!(tab.location, Location::Search(..)) {
children.push( children.push(menu_item(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), fl!("open-item-location"),
); Action::OpenItemLocation,
));
} }
// All selected items are directories // All selected items are directories
if selected == selected_dir && matches!(tab.mode, tab::Mode::App) { if selected == selected_dir && matches!(tab.mode, tab::Mode::App) {
children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab).into()); children.push(menu_item(fl!("open-in-new-tab"), Action::OpenInNewTab));
children children.push(menu_item(
.push(menu_item(fl!("open-in-new-window"), Action::OpenInNewWindow).into()); fl!("open-in-new-window"),
Action::OpenInNewWindow,
));
} }
let action_items = context_action_items(selected, selected_dir); let action_items = context_action_items(selected, selected_dir);
if !action_items.is_empty() { if !action_items.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.extend(action_items); children.extend(action_items);
} }
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
if selected_mount_point == 0 { if selected_mount_point == 0 {
children.push(menu_item(fl!("rename"), Action::Rename).into()); children.push(menu_item(fl!("rename"), Action::Rename));
children.push(menu_item(fl!("cut"), Action::Cut).into()); children.push(menu_item(fl!("cut"), Action::Cut));
} }
if modifiers.shift() && !modifiers.control() { if modifiers.shift() && !modifiers.control() {
children.push(menu_item(fl!("copy-path"), Action::CopyPath).into()); children.push(menu_item(fl!("copy-path"), Action::CopyPath));
} else { } else {
children.push(menu_item(fl!("copy"), Action::Copy).into()); children.push(menu_item(fl!("copy"), Action::Copy));
} }
if selected_mount_point == 0 { if selected_mount_point == 0 {
children.push(menu_item(fl!("move-to"), Action::MoveTo).into()); children.push(menu_item(fl!("move-to"), Action::MoveTo));
} }
children.push(menu_item(fl!("copy-to"), Action::CopyTo).into()); children.push(menu_item(fl!("copy-to"), Action::CopyTo));
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES; let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;
selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m)); selected_types.retain(|t| supported_archive_types.iter().copied().all(|m| *t != m));
if selected_types.is_empty() { if selected_types.is_empty() {
children.push(menu_item(fl!("extract-here"), Action::ExtractHere).into()); children.push(menu_item(fl!("extract-here"), Action::ExtractHere));
children.push(menu_item(fl!("extract-to"), Action::ExtractTo).into()); children.push(menu_item(fl!("extract-to"), Action::ExtractTo));
} }
children.push(menu_item(fl!("compress"), Action::Compress).into()); children.push(menu_item(fl!("compress"), Action::Compress));
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
//TODO: Print? //TODO: Print?
children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(menu_item(fl!("show-details"), Action::Preview));
if any_trash_item { if any_trash_item {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.push( children.push(menu_item(
menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into(), fl!("restore-from-trash"),
); Action::RestoreFromTrash,
children.push(divider::horizontal::light().into()); ));
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into()); children.push(menu::Item::Divider);
children.push(menu_item(fl!("delete-permanently"), Action::Delete));
} else { } else {
if matches!(tab.mode, tab::Mode::App) { if matches!(tab.mode, tab::Mode::App) {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar));
.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into());
} }
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
if tab.location.is_recents() { if tab.location.is_recents() {
children.push( children.push(menu_item(
menu_item(fl!("remove-from-recents"), Action::RemoveFromRecents).into(), fl!("remove-from-recents"),
); Action::RemoveFromRecents,
children.push(divider::horizontal::light().into()); ));
children.push(menu::Item::Divider);
} }
if selected_mount_point == 0 { if selected_mount_point == 0 {
if modifiers.shift() && !modifiers.control() { if modifiers.shift() && !modifiers.control() {
@ -312,55 +263,59 @@ pub fn context_menu<'a>(
.into(), .into(),
); );
} else { } else {
children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); children.push(menu_item(fl!("move-to-trash"), Action::Delete));
} }
} else if selected == 1 { } else if selected == 1 {
children.push(menu_item(fl!("eject"), Action::Eject).into()); children.push(menu_item(fl!("eject"), Action::Eject));
} }
} }
} else { } else {
//TODO: need better designs for menu with no selection //TODO: need better designs for menu with no selection
//TODO: have things like properties but they apply to the folder? //TODO: have things like properties but they apply to the folder?
if tab.location != Location::Recents { if tab.location != Location::Recents {
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); children.push(menu_item(fl!("new-folder"), Action::NewFolder));
children.push(menu_item(fl!("new-file"), Action::NewFile).into()); children.push(menu_item(fl!("new-file"), Action::NewFile));
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into()); children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal));
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
} }
if tab.mode.multiple() { if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); children.push(menu_item(fl!("select-all"), Action::SelectAll));
} }
if can_paste { if can_paste {
children.push(menu_item(fl!("paste"), Action::Paste).into()); children.push(menu_item(fl!("paste"), Action::Paste));
} else { } else {
children.push(menu_item_disabled(fl!("paste"), Action::Paste).into()); children.push(menu_item_disabled(fl!("paste"), Action::Paste));
} }
//TODO: only show if cosmic-settings is found? //TODO: only show if cosmic-settings is found?
if matches!(tab.mode, tab::Mode::Desktop) { if matches!(tab.mode, tab::Mode::Desktop) {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.push( children.push(menu_item(
menu_item(fl!("change-wallpaper"), Action::CosmicSettingsWallpaper).into(), fl!("change-wallpaper"),
); Action::CosmicSettingsWallpaper,
children.push( ));
menu_item(fl!("desktop-appearance"), Action::CosmicSettingsDesktop).into(), children.push(menu_item(
); fl!("desktop-appearance"),
children.push( Action::CosmicSettingsDesktop,
menu_item(fl!("display-settings"), Action::CosmicSettingsDisplays).into(), ));
); children.push(menu_item(
fl!("display-settings"),
Action::CosmicSettingsDisplays,
));
} }
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
// TODO: Nested menu // TODO: Nested menu
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size));
if matches!(tab.location, Location::Desktop(..)) { if matches!(tab.location, Location::Desktop(..)) {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.push( children.push(menu_item(
menu_item(fl!("desktop-view-options"), Action::DesktopViewOptions).into(), fl!("desktop-view-options"),
); Action::DesktopViewOptions,
));
} }
} }
} }
@ -375,24 +330,25 @@ pub fn context_menu<'a>(
) => { ) => {
if selected > 0 { if selected > 0 {
if selected_dir == 1 && selected == 1 || selected_dir == 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open));
} }
if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() { if matches!(tab.location, Location::Search(..)) || tab.location.is_recents() {
children.push( children.push(menu_item(
menu_item(fl!("open-item-location"), Action::OpenItemLocation).into(), fl!("open-item-location"),
); Action::OpenItemLocation,
));
} }
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(menu_item(fl!("show-details"), Action::Preview));
} else { } else {
if dialog_kind.save() { if dialog_kind.save() {
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into()); children.push(menu_item(fl!("new-folder"), Action::NewFolder));
} }
if tab.mode.multiple() { if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); children.push(menu_item(fl!("select-all"), Action::SelectAll));
} }
if !children.is_empty() { if !children.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
} }
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
@ -402,14 +358,14 @@ pub fn context_menu<'a>(
(_, Location::Network(..)) => { (_, Location::Network(..)) => {
if selected > 0 { if selected > 0 {
if selected_dir == 1 && selected == 1 || selected_dir == 0 { if selected_dir == 1 && selected == 1 || selected_dir == 0 {
children.push(menu_item(fl!("open"), Action::Open).into()); children.push(menu_item(fl!("open"), Action::Open));
} }
} else { } else {
if tab.mode.multiple() { if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); children.push(menu_item(fl!("select-all"), Action::SelectAll));
} }
if !children.is_empty() { if !children.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
} }
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified));
@ -418,24 +374,26 @@ pub fn context_menu<'a>(
} }
(_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => { (_, Location::Trash | Location::Search(SearchLocation::Trash, ..)) => {
if tab.mode.multiple() { if tab.mode.multiple() {
children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); children.push(menu_item(fl!("select-all"), Action::SelectAll));
} }
if !Trash::is_empty() { if !Trash::is_empty() {
if !children.is_empty() { if !children.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
} }
children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash).into()); children.push(menu_item(fl!("empty-trash"), Action::EmptyTrash));
} }
if !children.is_empty() { if !children.is_empty() {
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
} }
if selected > 0 { if selected > 0 {
children.push(menu_item(fl!("show-details"), Action::Preview).into()); children.push(menu_item(fl!("show-details"), Action::Preview));
children.push(divider::horizontal::light().into()); children.push(menu::Item::Divider);
children children.push(menu_item(
.push(menu_item(fl!("restore-from-trash"), Action::RestoreFromTrash).into()); fl!("restore-from-trash"),
children.push(divider::horizontal::light().into()); Action::RestoreFromTrash,
children.push(menu_item(fl!("delete-permanently"), Action::Delete).into()); ));
children.push(menu::Item::Divider);
children.push(menu_item(fl!("delete-permanently"), Action::Delete));
} else { } else {
// TODO: Nested menu // TODO: Nested menu
children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name));
@ -445,26 +403,11 @@ pub fn context_menu<'a>(
} }
} }
container(column::with_children(children)) let key_binds: HashMap<KeyBind, TabAction> = key_binds
.padding(1) .iter()
//TODO: move style to libcosmic .map(|(key_bind, action)| (key_bind.clone(), TabAction(*action)))
.style(|theme| { .collect();
let cosmic = theme.cosmic(); menu::items(&key_binds, children)
let component = &cosmic.background(false).component;
container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
})
.width(Length::Fixed(360.0))
.into()
} }
pub fn dialog_menu( pub fn dialog_menu(
@ -813,50 +756,33 @@ pub fn menu_bar<'a>(
) )
} }
pub fn location_context_menu<'a>(ancestor_index: usize) -> Element<'a, tab::Message> { pub fn location_context_menu(ancestor_index: usize) -> Vec<menu::Tree<tab::Message>> {
//TODO: only add some of these when in App mode //TODO: only add some of these when in App mode
let children = [ menu::items(
menu_button!(text::body(fl!("open-in-new-tab"))) &HashMap::new(),
.on_press(tab::Message::LocationMenuAction( vec![
menu::Item::Button(
fl!("open-in-new-tab"),
None,
LocationMenuAction::OpenInNewTab(ancestor_index), LocationMenuAction::OpenInNewTab(ancestor_index),
)) ),
.into(), menu::Item::Button(
menu_button!(text::body(fl!("open-in-new-window"))) fl!("open-in-new-window"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::OpenInNewWindow(ancestor_index), LocationMenuAction::OpenInNewWindow(ancestor_index),
)) ),
.into(), menu::Item::Divider,
divider::horizontal::light().into(), menu::Item::Button(
menu_button!(text::body(fl!("show-details"))) fl!("show-details"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::Preview(ancestor_index), LocationMenuAction::Preview(ancestor_index),
)) ),
.into(), menu::Item::Divider,
divider::horizontal::light().into(), menu::Item::Button(
menu_button!(text::body(fl!("add-to-sidebar"))) fl!("add-to-sidebar"),
.on_press(tab::Message::LocationMenuAction( None,
LocationMenuAction::AddToSidebar(ancestor_index), LocationMenuAction::AddToSidebar(ancestor_index),
)) ),
.into(), ],
]; )
container(column::with_children(children))
.padding(1)
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
})
.width(Length::Fixed(360.0))
.into()
} }

View file

@ -18,6 +18,15 @@ use crate::tab::{self, ChecksumState, DirSize, ItemMetadata, ItemThumbnail, Loca
const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri"; const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri";
// Attributes requested when listing a network directory.
const SCAN_ATTRIBUTES: &str = "standard::name,\
standard::display-name,\
standard::type,\
standard::size,\
standard::icon,\
standard::is-hidden,\
time::modified";
fn resolve_uri(uri: &str) -> (String, gio::File) { fn resolve_uri(uri: &str) -> (String, gio::File) {
let file = gio::File::for_uri(uri); let file = gio::File::for_uri(uri);
// Resolve the target-uri if it exists // Resolve the target-uri if it exists
@ -119,9 +128,27 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
Box::from([]) Box::from([])
}; };
// `filesystem::remote` belongs to the filesystem namespace, which `enumerate_children`
// never fills in, so it has to be queried once for the directory being listed.
let remote = file
.query_filesystem_info(
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
)
.map(|info| info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE))
.unwrap_or_else(|err| {
log::warn!("failed to get GIO filesystem info for {uri}: {err}");
// Assume remote, so per-entry work is skipped rather than retried
true
});
let mut items = Vec::new(); let mut items = Vec::new();
for info_res in file for info_res in file
.enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) .enumerate_children(
SCAN_ATTRIBUTES,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
)
.map_err(err_str)? .map_err(err_str)?
{ {
let info = info_res.map_err(err_str)?; let info = info_res.map_err(err_str)?;
@ -133,13 +160,17 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
//TODO: what is the best way to resolve shortcuts? //TODO: what is the best way to resolve shortcuts?
let location = Location::Network(uri, display_name.clone(), file.child(&name).path()); let location = Location::Network(uri, display_name.clone(), file.child(&name).path());
let metadata = if !force_dir && !info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE) { let metadata = if force_dir {
ItemMetadata::SimpleDir { entries: 0 }
} else {
let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let mtime = info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let is_dir = matches!(info.file_type(), gio::FileType::Directory); let is_dir = matches!(info.file_type(), gio::FileType::Directory);
let size_opt = (!is_dir).then_some(info.size() as u64); let size_opt = (!is_dir).then_some(info.size() as u64);
let mut children_opt = None; let mut children_opt = None;
if is_dir { // Counting children costs a directory listing per entry, which is far too
// expensive on a remote filesystem
if is_dir && !remote {
if let Some(path) = file.child(&name).path() { if let Some(path) = file.child(&name).path() {
//TODO: calculate children in the background (and make it cancellable?) //TODO: calculate children in the background (and make it cancellable?)
match std::fs::read_dir(&path) { match std::fs::read_dir(&path) {
@ -159,9 +190,8 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, String> {
mtime, mtime,
size_opt, size_opt,
children_opt, children_opt,
is_dir,
} }
} else {
ItemMetadata::SimpleDir { entries: 0 }
}; };
let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = {
@ -659,7 +689,7 @@ impl Mounter for Gvfs {
) )
} }
fn network_drive(&self, uri: String) -> Task<()> { fn network_drive(&self, uri: String) -> Task<bool> {
let command_tx = self.command_tx.clone(); let command_tx = self.command_tx.clone();
Task::perform( Task::perform(
async move { async move {
@ -668,9 +698,15 @@ impl Mounter for Gvfs {
command_tx.send(Cmd::NetworkDrive(uri, res_tx)).unwrap(); command_tx.send(Cmd::NetworkDrive(uri, res_tx)).unwrap();
res_rx.await res_rx.await
}, },
|x| { |result| match result {
if let Err(err) = x { Ok(Ok(())) => true,
Ok(Err(err)) => {
log::error!("{err:?}"); log::error!("{err:?}");
false
}
Err(err) => {
log::error!("{err:?}");
false
} }
}, },
) )

View file

@ -115,7 +115,7 @@ pub trait Mounter: Send + Sync {
fn items(&self, sizes: IconSizes) -> Option<MounterItems>; fn items(&self, sizes: IconSizes) -> Option<MounterItems>;
//TODO: send result //TODO: send result
fn mount(&self, item: MounterItem) -> Task<()>; fn mount(&self, item: MounterItem) -> Task<()>;
fn network_drive(&self, uri: String) -> Task<()>; fn network_drive(&self, uri: String) -> Task<bool>;
fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>; fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option<Result<Vec<tab::Item>, String>>;
fn dir_info(&self, uri: &str) -> Option<(String, String, Option<PathBuf>)>; fn dir_info(&self, uri: &str) -> Option<(String, String, Option<PathBuf>)>;
fn unmount(&self, item: MounterItem) -> Task<()>; fn unmount(&self, item: MounterItem) -> Task<()>;

View file

@ -214,8 +214,9 @@ impl<'a, Message, F> OnAutoScroll<'a, Message> for F where F: Fn(Option<f32>) ->
pub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {} pub trait OnMouseButton<'a, Message>: Fn(Option<Point>) -> Message + 'a {}
impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {} impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option<Point>) -> Message + 'a {}
pub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Message + 'a {} pub trait OnDrag<'a, Message>: Fn(Option<Rectangle>) -> Option<Message> + 'a {}
impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Message + 'a {} impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option<Rectangle>) -> Option<Message> + 'a
{}
pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {} pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {}
impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {} impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {}
@ -563,6 +564,16 @@ fn update<Message: Clone>(
state.last_position = position_in; state.last_position = position_in;
} }
if state.drag_initiated.is_some()
&& matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. })
)
{
shell.request_redraw();
}
if let Event::Mouse(mouse::Event::CursorMoved { position }) = event { if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {
let virtual_position = Point::new( let virtual_position = Point::new(
viewport.x - layout_bounds.x + position.x, viewport.x - layout_bounds.x + position.x,
@ -639,6 +650,7 @@ fn update<Message: Clone>(
{ {
state.drag_initiated = None; state.drag_initiated = None;
state.prev_click = None; state.prev_click = None;
shell.request_redraw();
if let Some(message) = widget.on_drag_end.as_ref() { if let Some(message) = widget.on_drag_end.as_ref() {
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
} }
@ -658,7 +670,9 @@ fn update<Message: Clone>(
state.prev_click = None; state.prev_click = None;
return; return;
} }
state.drag_initiated = None; if state.drag_initiated.take().is_some() {
shell.request_redraw();
}
if let Some(message) = widget.on_release.as_ref() { if let Some(message) = widget.on_release.as_ref() {
shell.publish(message(cursor.position_in(layout_bounds))); shell.publish(message(cursor.position_in(layout_bounds)));
@ -785,12 +799,13 @@ fn update<Message: Clone>(
} }
if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) { if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) {
shell.publish(message(drag_rect.intersection(&layout_bounds).map( let rect_opt = drag_rect.intersection(&layout_bounds).map(|mut rect| {
|mut rect| { rect.x -= layout_bounds.x;
rect.x -= layout_bounds.x; rect.y -= layout_bounds.y;
rect.y -= layout_bounds.y; rect
rect });
}, if let Some(message) = message(rect_opt) {
))); shell.publish(message);
}
} }
} }

View file

@ -454,7 +454,7 @@ impl Op {
} }
} }
let (from_file, metadata, to_file) = cosmic::iced::futures::join!( let (from_file_open_result, metadata, to_file_open_result) = cosmic::iced::futures::join!(
async { async {
compio::fs::OpenOptions::new() compio::fs::OpenOptions::new()
.read(true) .read(true)
@ -470,12 +470,30 @@ impl Op {
.write(true) .write(true)
.open(&self.to) .open(&self.to)
.await .await
.with_context(|| format!("failed to open {} for writing", self.to.display()))
} }
); );
let from_file = from_file?; let from_file = from_file_open_result?;
let mut to_file = to_file?;
let mut to_file = match to_file_open_result {
Ok(file) => file,
#[cfg(not(feature = "gvfs"))]
Err(why) => {
_ = from_file.close().await;
return Err(why)
.with_context(|| format!("failed to open {} for writing", self.to.display()))
.map_err(Into::into);
}
#[cfg(feature = "gvfs")]
Err(_why) => {
_ = from_file.close().await;
return self
.gio_file_copy(ctx, progress)
.await
.map(|()| true)
.map_err(Into::into);
}
};
progress.total_bytes = metadata.as_ref().map(|m| m.len()); progress.total_bytes = metadata.as_ref().map(|m| m.len());
(ctx.on_progress)(self, &progress); (ctx.on_progress)(self, &progress);

View file

@ -181,7 +181,9 @@ fn button_appearance(
appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color())); appearance.icon_color = Some(Color::from(cosmic.on_bg_component_color()));
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color())); appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
if cut { if cut {
appearance.text_color = Some(Color::from(cosmic.background(false).component.on_disabled)); appearance.text_color = Some(Color::from(
cosmic.background(theme.transparent).component.on_disabled,
));
} else { } else {
appearance.text_color = Some(Color::from(cosmic.on_bg_component_color())); appearance.text_color = Some(Color::from(cosmic.on_bg_component_color()));
} }
@ -192,12 +194,16 @@ fn button_appearance(
appearance.background = Some(Color::from(cosmic.bg_color()).into()); appearance.background = Some(Color::from(cosmic.bg_color()).into());
appearance.icon_color = Some(Color::from(cosmic.on_bg_color())); appearance.icon_color = Some(Color::from(cosmic.on_bg_color()));
if cut { if cut {
appearance.text_color = Some(Color::from(cosmic.background(false).component.disabled)); appearance.text_color = Some(Color::from(
cosmic.background(theme.transparent).component.disabled,
));
} else { } else {
appearance.text_color = Some(Color::from(cosmic.on_bg_color())); appearance.text_color = Some(Color::from(cosmic.on_bg_color()));
} }
} else if cut { } else if cut {
appearance.text_color = Some(Color::from(cosmic.background(false).component.on_disabled)); appearance.text_color = Some(Color::from(
cosmic.background(theme.transparent).component.on_disabled,
));
} }
if focused && accent { if focused && accent {
appearance.outline_width = 1.0; appearance.outline_width = 1.0;
@ -452,7 +458,9 @@ impl<'a> FormatTime<'a> {
impl Display for FormatTime<'_> { impl Display for FormatTime<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let zoned = jiff::Zoned::try_from(self.time).unwrap(); let Ok(zoned) = jiff::Zoned::try_from(self.time) else {
return Ok(());
};
let now = jiff::Zoned::now(); let now = jiff::Zoned::now();
let icu_datetime = DateTime::convert_from(zoned.datetime()); let icu_datetime = DateTime::convert_from(zoned.datetime());
if zoned.date() == now.date() { if zoned.date() == now.date() {
@ -655,6 +663,41 @@ fn display_name_for_file(path: &Path, name: &str, get_from_gvfs: bool, is_deskto
Item::display_name(name) Item::display_name(name)
} }
// Whether a dir lives on a remote filesystem, according to GIO.
#[cfg(feature = "gvfs")]
fn gvfs_dir_is_remote(dir: &Path) -> bool {
static REMOTE_CACHE: LazyLock<RwLock<FxHashMap<PathBuf, bool>>> =
LazyLock::new(|| RwLock::new(FxHashMap::default()));
if let Some(remote) = REMOTE_CACHE.read().unwrap().get(dir) {
return *remote;
}
let remote = match gio::prelude::FileExt::query_filesystem_info(
&gio::File::for_path(dir),
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
) {
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
Err(err) => {
log::warn!(
"failed to get GIO filesystem info for {}: {}",
dir.display(),
err
);
//Assume remote so that the "expensive tasks" are rather skipped then actually executed.
true
}
};
REMOTE_CACHE
.write()
.unwrap()
.insert(dir.to_path_buf(), remote);
remote
}
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item { pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconSizes) -> Item {
let file_name = file_info let file_name = file_info
@ -662,7 +705,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
.unwrap_or_default(); .unwrap_or_default();
let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED); let mtime = file_info.attribute_uint64(gio::FILE_ATTRIBUTE_TIME_MODIFIED);
let mut is_desktop = false; let mut is_desktop = false;
let remote = file_info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE); let remote = path.parent().is_none_or(gvfs_dir_is_remote);
let is_dir = matches!(file_info.file_type(), gio::FileType::Directory); let is_dir = matches!(file_info.file_type(), gio::FileType::Directory);
let size_opt = (!is_dir).then_some(file_info.size() as u64); let size_opt = (!is_dir).then_some(file_info.size() as u64);
@ -723,6 +766,7 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS
mtime, mtime,
size_opt, size_opt,
children_opt, children_opt,
is_dir,
}, },
hidden, hidden,
image_dimensions: (!remote && mime.type_() == mime::IMAGE) image_dimensions: (!remote && mime.type_() == mime::IMAGE)
@ -777,23 +821,7 @@ pub fn item_from_entry(
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
FsKind::Gvfs => { FsKind::Gvfs => {
is_gvfs = true; is_gvfs = true;
let file = gio::File::for_path(&path); path.parent().is_none_or(gvfs_dir_is_remote)
match gio::prelude::FileExt::query_filesystem_info(
&file,
gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE,
gio::Cancellable::NONE,
) {
Ok(info) => info.boolean(gio::FILE_ATTRIBUTE_FILESYSTEM_REMOTE),
Err(err) => {
log::warn!(
"failed to get GIO filesystem info for {}: {}",
path.display(),
err
);
true
}
}
} }
#[cfg(not(feature = "gvfs"))] #[cfg(not(feature = "gvfs"))]
FsKind::Gvfs => { FsKind::Gvfs => {
@ -945,11 +973,24 @@ fn item_from_trash_child(
metadata: fs::Metadata, metadata: fs::Metadata,
sizes: IconSizes, sizes: IconSizes,
) -> Option<Item> { ) -> Option<Item> {
let original_path = crate::trash::original_path_for_trash_child(&path)?; let Some(original_path) = crate::trash::original_path_for_trash_child(&path) else {
log::warn!(
"failed to resolve original path for trash item {}, skipping entry",
path.display()
);
return None;
};
let Some(original_parent) = original_path.parent() else {
log::warn!(
"trash item {} has no original parent, skipping entry",
path.display()
);
return None;
};
let entry = trash::TrashItem { let entry = trash::TrashItem {
id: path.as_os_str().to_os_string(), id: path.as_os_str().to_os_string(),
name: std::ffi::OsString::from(&name), name: std::ffi::OsString::from(&name),
original_parent: original_path.parent()?.to_path_buf(), original_parent: original_parent.to_path_buf(),
time_deleted: 0, time_deleted: 0,
}; };
let size = if metadata.is_dir() { let size = if metadata.is_dir() {
@ -1005,7 +1046,6 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
// gio crate expects a comma delimited string // gio crate expects a comma delimited string
let attr_string = [ let attr_string = [
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME.as_str(), 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_TIME_MODIFIED.as_str(),
gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(), gio::FILE_ATTRIBUTE_STANDARD_SIZE.as_str(),
gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(), gio::FILE_ATTRIBUTE_STANDARD_TYPE.as_str(),
@ -1042,6 +1082,7 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
if !remote_scannable { if !remote_scannable {
match fs::read_dir(tab_path) { match fs::read_dir(tab_path) {
Ok(entries) => { Ok(entries) => {
let trash = crate::trash::is_trash_path(tab_path);
items = entries items = entries
.filter_map(|entry_res| { .filter_map(|entry_res| {
let entry = entry_res let entry = entry_res
@ -1082,7 +1123,6 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
}) })
.ok()?; .ok()?;
let trash = crate::trash::is_trash_path(tab_path);
if trash { if trash {
item_from_trash_child(path, name, metadata, sizes) item_from_trash_child(path, name, metadata, sizes)
} else { } else {
@ -1717,11 +1757,11 @@ impl fmt::Debug for TaskWrapper {
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
Action(Action), Action(Action),
Surface(cosmic::surface::Action<Message>),
AddNetworkDrive, AddNetworkDrive,
AddToSidebar(PathBuf), AddToSidebar(PathBuf),
AutoScroll(Option<f32>), AutoScroll(Option<f32>),
ChangeLocation(String, Location, Option<Vec<PathBuf>>), ChangeLocation(String, Location, Option<Vec<PathBuf>>),
ContextMenu(Option<Point>, Option<window::Id>),
Delete(Vec<PathBuf>), Delete(Vec<PathBuf>),
DropFiles(PathBuf, ClipboardPaste), DropFiles(PathBuf, ClipboardPaste),
ClearRecents, ClearRecents,
@ -1752,9 +1792,9 @@ pub enum Message {
ClickRelease(Option<usize>), ClickRelease(Option<usize>),
Config(TabConfig), Config(TabConfig),
ContextAction(Action), ContextAction(Action),
ContextMenu(Option<Point>, Option<window::Id>), RightClickBackground,
LocationContextMenuPoint(Option<Point>), Surface(cosmic::surface::Action<Message>),
LocationContextMenuIndex(Option<Point>, Option<usize>), LocationContextMenuIndex(Option<usize>),
LocationMenuAction(LocationMenuAction), LocationMenuAction(LocationMenuAction),
Drag(Option<Rectangle>), Drag(Option<Rectangle>),
DragEnd, DragEnd,
@ -1814,6 +1854,8 @@ pub enum Message {
HighlightDeactivate(usize), HighlightDeactivate(usize),
HighlightActivate(usize), HighlightActivate(usize),
DirectorySize(PathBuf, DirSize), DirectorySize(PathBuf, DirSize),
#[cfg(feature = "gvfs")]
DirectoryChildren(PathBuf, usize),
Checksums(PathBuf, ChecksumState), Checksums(PathBuf, ChecksumState),
CalculateChecksums(PathBuf), CalculateChecksums(PathBuf),
CopyChecksum(String), CopyChecksum(String),
@ -1882,6 +1924,7 @@ pub enum ItemMetadata {
mtime: u64, mtime: u64,
size_opt: Option<u64>, size_opt: Option<u64>,
children_opt: Option<usize>, children_opt: Option<usize>,
is_dir: bool,
}, },
} }
@ -1896,7 +1939,7 @@ impl ItemMetadata {
Self::SimpleDir { .. } => true, Self::SimpleDir { .. } => true,
Self::SimpleFile { .. } => false, Self::SimpleFile { .. } => false,
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
Self::GvfsPath { children_opt, .. } => children_opt.is_some(), Self::GvfsPath { is_dir, .. } => *is_dir,
} }
} }
@ -1905,7 +1948,7 @@ impl ItemMetadata {
Self::Path { metadata, .. } => metadata.modified().ok(), Self::Path { metadata, .. } => metadata.modified().ok(),
#[cfg(feature = "gvfs")] #[cfg(feature = "gvfs")]
Self::GvfsPath { mtime, .. } => { Self::GvfsPath { mtime, .. } => {
Some(SystemTime::UNIX_EPOCH + Duration::from_secs(*mtime)) SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(*mtime))
} }
_ => None, _ => None,
} }
@ -2327,6 +2370,7 @@ impl Item {
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> { ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
widget::text::body(name) widget::text::body(name)
.wrapping(text::Wrapping::WordOrGlyph) .wrapping(text::Wrapping::WordOrGlyph)
.align_x(text::Alignment::Center)
.ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines( .ellipsize(text::Ellipsize::Middle(text::EllipsizeHeightLimit::Lines(
3, 3,
))) )))
@ -2382,10 +2426,8 @@ impl Item {
widget::image(handle.clone()).into() widget::image(handle.clone()).into()
} }
ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(), ItemThumbnail::Svg(handle) => widget::svg(handle.clone()).into(),
ItemThumbnail::Text(content) => widget::text_editor(content) ItemThumbnail::Text(content) => widget::text_editor::text_editor(content)
.class(cosmic::theme::iced::TextEditor::Custom(Box::new( .style(text_editor_class)
text_editor_class,
)))
.width(THUMBNAIL_SIZE as f32) .width(THUMBNAIL_SIZE as f32)
.height(Length::Fixed(THUMBNAIL_SIZE as f32)) .height(Length::Fixed(THUMBNAIL_SIZE as f32))
.padding(spacing.space_xxs) .padding(spacing.space_xxs)
@ -2436,7 +2478,7 @@ impl Item {
); );
let mut details = widget::column::with_capacity(8).spacing(space_xxxs); let mut details = widget::column::with_capacity(8).spacing(space_xxxs);
details = details.push(widget::text::heading(self.name.clone())); details = details.push(widget::selectable_text::heading(self.name.clone()));
details = details.push(widget::text::body(fl!( details = details.push(widget::text::body(fl!(
"type", "type",
mime = self.mime.to_string() mime = self.mime.to_string()
@ -2493,21 +2535,21 @@ impl Item {
let time_formatter = time_formatter(military_time); let time_formatter = time_formatter(military_time);
if let Ok(time) = metadata.created() { if let Ok(time) = metadata.created() {
details = details.push(widget::text::body(fl!( details = details.push(widget::selectable_text::body(fl!(
"item-created", "item-created",
created = format_time(time, &date_time_formatter, &time_formatter).to_string() created = format_time(time, &date_time_formatter, &time_formatter).to_string()
))); )));
} }
if let Ok(time) = metadata.modified() { if let Ok(time) = metadata.modified() {
details = details.push(widget::text::body(fl!( details = details.push(widget::selectable_text::body(fl!(
"item-modified", "item-modified",
modified = format_time(time, &date_time_formatter, &time_formatter).to_string() modified = format_time(time, &date_time_formatter, &time_formatter).to_string()
))); )));
} }
if let Ok(time) = metadata.accessed() { if let Ok(time) = metadata.accessed() {
details = details.push(widget::text::body(fl!( details = details.push(widget::selectable_text::body(fl!(
"item-accessed", "item-accessed",
accessed = format_time(time, &date_time_formatter, &time_formatter).to_string() accessed = format_time(time, &date_time_formatter, &time_formatter).to_string()
))); )));
@ -2778,9 +2820,8 @@ pub struct Tab {
pub location: Location, pub location: Location,
pub location_ancestors: Vec<(Location, String)>, pub location_ancestors: Vec<(Location, String)>,
pub location_title: String, pub location_title: String,
pub location_context_menu_point: Option<Point>, /// Breadcrumb whose context menu is open, drawn as active while it shows
pub location_context_menu_index: Option<usize>, pub location_context_menu_index: Option<usize>,
pub context_menu: Option<Point>,
pub mode: Mode, pub mode: Mode,
pub scroll_opt: Option<AbsoluteOffset>, pub scroll_opt: Option<AbsoluteOffset>,
pub size_opt: Cell<Option<Size>>, pub size_opt: Cell<Option<Size>>,
@ -2808,7 +2849,6 @@ pub struct Tab {
search_context: Option<SearchContext>, search_context: Option<SearchContext>,
date_time_formatter: DateTimeFormatter<fieldsets::YMDT>, date_time_formatter: DateTimeFormatter<fieldsets::YMDT>,
time_formatter: DateTimeFormatter<fieldsets::T>, time_formatter: DateTimeFormatter<fieldsets::T>,
watch_drag: bool,
window_id: Option<window::Id>, window_id: Option<window::Id>,
large_image_manager: LargeImageManager, large_image_manager: LargeImageManager,
} }
@ -2925,8 +2965,6 @@ impl Tab {
location, location,
location_ancestors, location_ancestors,
location_title, location_title,
context_menu: None,
location_context_menu_point: None,
location_context_menu_index: None, location_context_menu_index: None,
mode: Mode::App, mode: Mode::App,
scroll_opt: None, scroll_opt: None,
@ -2955,7 +2993,6 @@ impl Tab {
search_context: None, search_context: None,
date_time_formatter: date_time_formatter(config.military_time), date_time_formatter: date_time_formatter(config.military_time),
time_formatter: time_formatter(config.military_time), time_formatter: time_formatter(config.military_time),
watch_drag: true,
window_id, window_id,
large_image_manager: LargeImageManager::new(), large_image_manager: LargeImageManager::new(),
} }
@ -3468,7 +3505,6 @@ impl Tab {
self.location = location.normalize(); self.location = location.normalize();
self.location_ancestors = self.location.ancestors(); self.location_ancestors = self.location.ancestors();
self.location_title = self.location.title(); self.location_title = self.location.title();
self.context_menu = None;
self.edit_location = None; self.edit_location = None;
self.items_opt = None; self.items_opt = None;
//TODO: remember scroll by location? //TODO: remember scroll by location?
@ -3513,7 +3549,6 @@ impl Tab {
let mut history_i_opt = None; let mut history_i_opt = None;
let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple(); let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple();
let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple();
let last_context_menu = self.context_menu;
match message { match message {
Message::AddNetworkDrive => { Message::AddNetworkDrive => {
commands.push(Command::AddNetworkDrive); commands.push(Command::AddNetworkDrive);
@ -3548,8 +3583,6 @@ impl Tab {
} }
if click_i_opt != self.clicked.take() { if click_i_opt != self.clicked.take() {
self.context_menu = None;
self.location_context_menu_index = None;
if let Some(ref mut items) = self.items_opt { if let Some(ref mut items) = self.items_opt {
for (i, item) in items.iter_mut().enumerate() { for (i, item) in items.iter_mut().enumerate() {
if mod_ctrl { if mod_ctrl {
@ -3566,7 +3599,6 @@ impl Tab {
} }
Message::DragEnd => { Message::DragEnd => {
self.clicked = None; self.clicked = None;
self.watch_drag = true;
} }
Message::DoubleClick(click_i_opt) => { Message::DoubleClick(click_i_opt) => {
if let Some(clicked_item) = self if let Some(clicked_item) = self
@ -3591,9 +3623,7 @@ impl Tab {
} }
Message::Click(click_i_opt) => { Message::Click(click_i_opt) => {
self.selected_clicked = false; self.selected_clicked = false;
self.context_menu = None;
self.edit_location = None; self.edit_location = None;
self.location_context_menu_index = None;
if click_i_opt.is_none() { if click_i_opt.is_none() {
self.clicked = click_i_opt; self.clicked = click_i_opt;
} }
@ -3735,24 +3765,16 @@ impl Tab {
} }
} }
Message::ContextAction(action) => { Message::ContextAction(action) => {
// Close context menu
self.context_menu = None;
commands.push(Command::Action(action)); commands.push(Command::Action(action));
} }
Message::RunContextAction(action) => { Message::RunContextAction(action) => {
self.context_menu = None;
commands.push(Command::RunContextAction(action)); commands.push(Command::RunContextAction(action));
} }
Message::ContextMenu(point_opt, _) => { Message::RightClickBackground => {
self.edit_location = None; self.edit_location = None;
self.context_menu = point_opt;
self.location_context_menu_index = None;
//TODO: hack for clearing selecting when right clicking empty space //TODO: hack for clearing selecting when right clicking empty space
if self.context_menu.is_some() if self.last_right_click.take().is_none()
&& self.last_right_click.take().is_none()
&& let Some(ref mut items) = self.items_opt && let Some(ref mut items) = self.items_opt
{ {
for item in items.iter_mut() { for item in items.iter_mut() {
@ -3760,17 +3782,13 @@ impl Tab {
} }
} }
} }
Message::LocationContextMenuPoint(point_opt) => { Message::Surface(action) => {
self.context_menu = None; commands.push(Command::Surface(action));
self.location_context_menu_point = point_opt;
} }
Message::LocationContextMenuIndex(p, index_opt) => { Message::LocationContextMenuIndex(index) => {
self.context_menu = None; self.location_context_menu_index = index;
self.location_context_menu_point = p;
self.location_context_menu_index = index_opt;
} }
Message::LocationMenuAction(action) => { Message::LocationMenuAction(action) => {
self.location_context_menu_index = None;
let path_for_index = |ancestor_index| { let path_for_index = |ancestor_index| {
self.location self.location
.path_opt() .path_opt()
@ -3820,10 +3838,7 @@ impl Tab {
} }
} }
Message::Drag(rect_opt) => { Message::Drag(rect_opt) => {
self.watch_drag = false;
if let Some(rect) = rect_opt { if let Some(rect) = rect_opt {
self.context_menu = None;
self.location_context_menu_index = None;
if self.mode.multiple() { if self.mode.multiple() {
self.select_rect(rect, mod_ctrl, mod_shift); self.select_rect(rect, mod_ctrl, mod_shift);
} }
@ -3859,6 +3874,28 @@ impl Tab {
} }
Message::EditLocationSubmit => { Message::EditLocationSubmit => {
if let Some(mut edit_location) = self.edit_location.take() { if let Some(mut edit_location) = self.edit_location.take() {
let typed_opt = match &edit_location.location {
Location::Path(path) => path.to_str().map(str::to_string),
Location::Network(uri, ..) => Some(uri.clone()),
_ => None,
};
let mut typed_uri = false;
if let Some(typed) = typed_opt {
match typed.trim().parse::<url::Url>() {
Ok(url) if url.scheme() != "file" && url.has_host() => {
let uri = url.as_str().to_string();
edit_location =
Location::Network(uri.clone(), uri, None).normalize().into();
typed_uri = true;
}
Err(_) if matches!(edit_location.location, Location::Network(..)) => {
edit_location =
Location::Path(PathBuf::from(typed)).normalize().into();
}
_ => {}
}
}
// Select first completion if current location does not exist // Select first completion if current location does not exist
if edit_location.selected.is_none() if edit_location.selected.is_none()
&& edit_location && edit_location
@ -3874,6 +3911,9 @@ impl Tab {
} }
cd = edit_location.resolve(); cd = edit_location.resolve();
if cd.is_none() && typed_uri {
cd = Some(edit_location.location);
}
} }
} }
Message::EditLocationTab => { Message::EditLocationTab => {
@ -4435,13 +4475,11 @@ impl Tab {
} }
} }
Message::HighlightDeactivate(i) => { Message::HighlightDeactivate(i) => {
self.watch_drag = true;
if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) { if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {
item.highlighted = false; item.highlighted = false;
} }
} }
Message::HighlightActivate(i) => { Message::HighlightActivate(i) => {
self.watch_drag = true;
if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) { if let Some(item) = self.items_opt.as_mut().and_then(|f| f.get_mut(i)) {
item.highlighted = true; item.highlighted = true;
} }
@ -4467,7 +4505,6 @@ impl Tab {
} }
Message::Scroll(viewport) => { Message::Scroll(viewport) => {
self.scroll_opt = Some(viewport.absolute_offset()); self.scroll_opt = Some(viewport.absolute_offset());
self.watch_drag = true;
} }
Message::ScrollTab(scroll_speed) => { Message::ScrollTab(scroll_speed) => {
commands.push(Command::Iced( commands.push(Command::Iced(
@ -4800,6 +4837,20 @@ impl Tab {
} }
} }
} }
#[cfg(feature = "gvfs")]
Message::DirectoryChildren(path, children) => {
if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() {
if item.path_opt() == Some(&path) {
if let ItemMetadata::GvfsPath { children_opt, .. } = &mut item.metadata
{
*children_opt = Some(children);
}
break;
}
}
}
}
Message::Checksums(path, checksum_state) => { Message::Checksums(path, checksum_state) => {
let location = Location::Path(path); let location = Location::Path(path);
if let Some(ref mut item) = self.parent_item_opt if let Some(ref mut item) = self.parent_item_opt
@ -4906,16 +4957,6 @@ impl Tab {
} }
} }
// Update context menu popup
if self.context_menu != last_context_menu {
if last_context_menu.is_some() {
commands.push(Command::ContextMenu(None, self.window_id));
}
if let Some(point) = self.context_menu {
commands.push(Command::ContextMenu(Some(point), self.window_id));
}
}
commands commands
} }
@ -4961,11 +5002,15 @@ impl Tab {
ItemMetadata::GvfsPath { ItemMetadata::GvfsPath {
size_opt, size_opt,
children_opt, children_opt,
is_dir,
.. ..
} => match children_opt { } => {
Some(child_count) => (true, *child_count as u64), if *is_dir {
None => (false, size_opt.unwrap_or_default()), (true, children_opt.unwrap_or_default() as u64)
}, } else {
(false, size_opt.unwrap_or_default())
}
}
}; };
let (a_is_entry, a_size) = get_size(a.1); let (a_is_entry, a_size) = get_size(a.1);
let (b_is_entry, b_size) = get_size(b.1); let (b_is_entry, b_size) = get_size(b.1);
@ -5174,9 +5219,11 @@ impl Tab {
} }
ItemThumbnail::Text(text) => { ItemThumbnail::Text(text) => {
element_opt = Some( element_opt = Some(
widget::container(widget::text_editor(text).padding(space_xxs).class( widget::container(
cosmic::theme::iced::TextEditor::Custom(Box::new(text_editor_class)), widget::text_editor::text_editor(text)
)) .padding(space_xxs)
.style(text_editor_class),
)
.center(Length::Fill) .center(Length::Fill)
.into(), .into(),
); );
@ -5520,10 +5567,14 @@ impl Tab {
} }
let location = self.location.with_path(ancestor.to_path_buf()); let location = self.location.with_path(ancestor.to_path_buf());
let mut mouse_area = crate::mouse_area::MouseArea::new( let mouse_area = crate::mouse_area::MouseArea::new(
widget::button::custom(row) widget::button::custom(row)
.padding(space_xxxs) .padding(space_xxxs)
.class(theme::Button::Link) .class(if self.location_context_menu_index == Some(index) {
theme::Button::LinkActive
} else {
theme::Button::Link
})
.on_press(if ancestor == path { .on_press(if ancestor == path {
Message::EditLocation(Some(self.location.clone().into())) Message::EditLocation(Some(self.location.clone().into()))
} else { } else {
@ -5531,21 +5582,6 @@ impl Tab {
}), }),
); );
if self.location_context_menu_index.is_some() {
mouse_area = mouse_area
.on_right_press(move |point_opt| {
Message::LocationContextMenuIndex(point_opt, None)
})
.wayland_on_right_press_window_position();
} else {
mouse_area = mouse_area
.on_right_press_no_capture()
.on_right_press(move |point_opt| {
Message::LocationContextMenuIndex(point_opt, Some(index))
})
.wayland_on_right_press_window_position();
}
let mouse_area = if let Location::Path(_) = &self.location { let mouse_area = if let Location::Path(_) = &self.location {
mouse_area mouse_area
.on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf())) .on_middle_press(move |_| Message::OpenInNewTab(ancestor.to_path_buf()))
@ -5553,7 +5589,16 @@ impl Tab {
mouse_area mouse_area
}; };
children.push(self.dnd_dest(&location, mouse_area)); // Each breadcrumb carries the menu for its own ancestor index
let mut context_menu =
widget::context_menu(mouse_area, Some(menu::location_context_menu(index)))
.on_open(Message::LocationContextMenuIndex(Some(index)))
.on_close(Message::LocationContextMenuIndex(None))
.on_surface_action(Message::Surface);
if let Some(window_id) = self.window_id {
context_menu = context_menu.window_id(window_id);
}
children.push(self.dnd_dest(&location, context_menu));
if found_home || overflow { if found_home || overflow {
break; break;
@ -5604,20 +5649,7 @@ impl Tab {
column = column.push(heading_rule); column = column.push(heading_rule);
} }
let mouse_area = crate::mouse_area::MouseArea::new(column) column.into()
.on_right_press(Message::LocationContextMenuPoint);
let mut popover = widget::popover(mouse_area);
if let (Some(point), Some(index)) = (
self.location_context_menu_point,
self.location_context_menu_index,
) {
popover = popover
.popup(menu::location_context_menu(index))
.position(widget::popover::Position::Point(point));
}
popover.into()
} }
pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> { pub fn empty_view(&self, has_hidden: bool) -> Element<'_, Message> {
@ -5806,18 +5838,13 @@ impl Tab {
.height(Length::Fixed(item_height as f32)) .height(Length::Fixed(item_height as f32))
.width(Length::Fixed(item_width as f32)); .width(Length::Fixed(item_width as f32));
for button in buttons { for button in buttons {
if self.context_menu.is_some() { column = column.push(
column = column.push(button); mouse_area::MouseArea::new(button)
} else { .on_right_press_no_capture()
column = column.push( .on_right_press(move |point_opt| {
mouse_area::MouseArea::new(button) Message::RightClick(point_opt, Some(i))
.on_right_press_no_capture() }),
.wayland_on_right_press_window_position() );
.on_right_press(move |point_opt| {
Message::RightClick(point_opt, Some(i))
}),
);
}
} }
let column: Element<Message> = let column: Element<Message> =
@ -5984,15 +6011,13 @@ impl Tab {
Element::from(dnd_grid) Element::from(dnd_grid)
}); });
let mut mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill)) let mouse_area = mouse_area::MouseArea::new(column.width(Length::Fill))
.on_press(|_| Message::Click(None)) .on_press(|_| Message::Click(None))
.on_auto_scroll(Message::AutoScroll) .on_auto_scroll(Message::AutoScroll)
.on_drag(move |rect_opt| self.on_drag(rect_opt))
.on_drag_end(|_| Message::DragEnd) .on_drag_end(|_| Message::DragEnd)
.show_drag_rect(self.mode.multiple()) .show_drag_rect(self.mode.multiple())
.on_release(|_| Message::ClickRelease(None)); .on_release(|_| Message::ClickRelease(None));
if self.watch_drag {
mouse_area = mouse_area.on_drag(Message::Drag);
}
(drag_list, mouse_area.into(), true) (drag_list, mouse_area.into(), true)
} }
@ -6141,17 +6166,21 @@ impl Tab {
ItemMetadata::GvfsPath { ItemMetadata::GvfsPath {
size_opt, size_opt,
children_opt, children_opt,
is_dir,
.. ..
} => match children_opt { } => {
Some(child_count) => { if *is_dir {
if *child_count == 1 { // Children are not counted on remote filesystems
format!("{child_count} item") match children_opt {
} else { //TODO: translate
format!("{child_count} items") Some(1) => "1 item".to_string(),
Some(child_count) => format!("{child_count} items"),
None => String::new(),
} }
} else {
format_size(size_opt.unwrap_or_default())
} }
None => format_size(size_opt.unwrap_or_default()), }
},
}; };
let row = if condensed { let row = if condensed {
@ -6218,39 +6247,33 @@ impl Tab {
.spacing(space_xxs) .spacing(space_xxs)
}; };
let button = |row| { let button =
let mouse_area = crate::mouse_area::MouseArea::new( |row| {
widget::button::custom(row) let mouse_area = crate::mouse_area::MouseArea::new(
.width(Length::Fill) widget::button::custom(row)
.id(item.button_id.clone()) .width(Length::Fill)
.padding([0, space_xxs]) .id(item.button_id.clone())
.class(button_style( .padding([0, space_xxs])
item.selected, .class(button_style(
item.highlighted, item.selected,
item.cut, item.highlighted,
true, item.cut,
true, true,
false, true,
)), false,
) )),
.on_press(move |_| Message::Click(Some(i))) )
.on_double_click(move |_| Message::DoubleClick(Some(i))) .on_press(move |_| Message::Click(Some(i)))
.on_release(move |_| Message::ClickRelease(Some(i))) .on_double_click(move |_| Message::DoubleClick(Some(i)))
.on_middle_press(move |_| Message::MiddleClick(i)) .on_release(move |_| Message::ClickRelease(Some(i)))
.on_enter(move || Message::HighlightActivate(i)) .on_middle_press(move |_| Message::MiddleClick(i))
.on_exit(move || Message::HighlightDeactivate(i)); .on_enter(move || Message::HighlightActivate(i))
.on_exit(move || Message::HighlightDeactivate(i));
if self.context_menu.is_some() { mouse_area.on_right_press_no_capture().on_right_press(
mouse_area move |point_opt| Message::RightClick(point_opt, Some(i)),
} else { )
mouse_area };
.on_right_press_no_capture()
.wayland_on_right_press_window_position()
.on_right_press(move |point_opt| {
Message::RightClick(point_opt, Some(i))
})
}
};
let button_row = button(row.into()); let button_row = button(row.into());
let button_row: Element<_> = if item.metadata.is_dir() let button_row: Element<_> = if item.metadata.is_dir()
@ -6378,16 +6401,14 @@ impl Tab {
let drag_col = (!drag_items.is_empty()) let drag_col = (!drag_items.is_empty())
.then(|| Element::from(widget::column::with_children(drag_items))); .then(|| Element::from(widget::column::with_children(drag_items)));
let mut mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s])) let mouse_area = mouse_area::MouseArea::new(column.padding([0, space_s]))
.with_id(Id::new("list-view")) .with_id(Id::new("list-view"))
.on_press(|_| Message::Click(None)) .on_press(|_| Message::Click(None))
.on_auto_scroll(Message::AutoScroll) .on_auto_scroll(Message::AutoScroll)
.on_drag(move |rect_opt| self.on_drag(rect_opt))
.on_drag_end(|_| Message::DragEnd) .on_drag_end(|_| Message::DragEnd)
.show_drag_rect(self.mode.multiple()) .show_drag_rect(self.mode.multiple())
.on_release(|_| Message::ClickRelease(None)); .on_release(|_| Message::ClickRelease(None));
if self.watch_drag {
mouse_area = mouse_area.on_drag(Message::Drag);
}
(drag_col, mouse_area.into(), true) (drag_col, mouse_area.into(), true)
} }
@ -6474,51 +6495,47 @@ impl Tab {
.on_back_press(move |_point_opt| Message::GoPrevious) .on_back_press(move |_point_opt| Message::GoPrevious)
.on_forward_press(move |_point_opt| Message::GoNext) .on_forward_press(move |_point_opt| Message::GoNext)
.on_scroll(|delta| respond_to_scroll_direction(delta, modifiers)) .on_scroll(|delta| respond_to_scroll_direction(delta, modifiers))
.on_right_press(move |p| { .on_right_press(|_| Message::RightClickBackground);
Message::ContextMenu(
if self.context_menu.is_some() { None } else { p },
self.window_id,
)
})
.wayland_on_right_press_window_position();
let mut popover = widget::popover(mouse_area); let items_area: Element<'_, Message> = if can_scroll {
if let Some(point) = self.context_menu // FIXME: new responsive widget will remove the state from the scrollable
&& (!cfg!(feature = "wayland") || !crate::is_wayland()) // id_container with custom id forces the state to be extracted in a diff
{ // pre-processing step
let context_menu = menu::context_menu( widget::id_container(
widget::scrollable(mouse_area)
.id(self.scrollable_id.clone())
.on_scroll(Message::Scroll)
.width(Length::Fill)
.height(Length::Fill),
widget::Id::new(format!("{}-scrollable", self.scrollable_id)),
)
.into()
} else {
mouse_area.into()
};
// Wrap the scrollable, not its content, so the popup anchors in window coordinates
let mut context_menu = widget::context_menu(
items_area,
Some(menu::context_menu(
self, self,
key_binds, key_binds,
modifiers, modifiers,
clipboard_paste_available, clipboard_paste_available,
context_actions, context_actions,
); )),
popover = popover )
.popup(context_menu) .item_width(cosmic::widget::menu::ItemWidth::Uniform(360))
.position(widget::popover::Position::Point(point)); .on_surface_action(Message::Surface);
if let Some(window_id) = self.window_id {
context_menu = context_menu.window_id(window_id);
} }
let mut tab_column = widget::column::with_capacity(3); let mut tab_column = widget::column::with_capacity(3);
if let Some(location_view) = location_view_opt { if let Some(location_view) = location_view_opt {
tab_column = tab_column.push(location_view); tab_column = tab_column.push(location_view);
} }
if can_scroll { tab_column = tab_column.push(context_menu);
tab_column = tab_column.push(
// FIXME: new responsive widget will remove the state from the scrollable
// id_container with custom id forces the state to be extracted in a diff
// pre-processing step
widget::id_container(
widget::scrollable(popover)
.id(self.scrollable_id.clone())
.on_scroll(Message::Scroll)
.width(Length::Fill)
.height(Length::Fill),
widget::Id::new(format!("{}-scrollable", self.scrollable_id)),
),
);
} else {
tab_column = tab_column.push(popover);
}
match &self.location { match &self.location {
Location::Trash | Location::Search(SearchLocation::Trash, ..) => { Location::Trash | Location::Search(SearchLocation::Trash, ..) => {
if let Some(items) = self.items_opt() if let Some(items) = self.items_opt()
@ -6917,6 +6934,74 @@ impl Tab {
Rectangle::new(point, size) Rectangle::new(point, size)
}; };
// Count the children of visible directories in the background. Doing it while
// scanning costs one directory listing per entry, which stalls remote filesystems.
#[cfg(feature = "gvfs")]
for item in items {
let ItemMetadata::GvfsPath {
children_opt: None,
is_dir: true,
..
} = &item.metadata
else {
continue;
};
// Skip items that are not visible, or have no determined rect
match item.rect_opt.get() {
Some(rect) if rect.intersects(&visible_rect) => {}
_ => continue,
}
let Some(path) = item.path_opt().cloned() else {
continue;
};
struct ChildrenWrapper(PathBuf);
impl Hash for ChildrenWrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
subscriptions.push(Subscription::run_with(
ChildrenWrapper(path),
|ChildrenWrapper(path)| {
let path = path.clone();
stream::channel(
1,
move |mut output: futures::channel::mpsc::Sender<_>| async move {
let message = {
let path = path.clone();
tokio::task::spawn_blocking(move || {
let children = match fs::read_dir(&path) {
Ok(entries) => entries.count(),
Err(err) => {
log::warn!(
"failed to read directory {}: {}",
path.display(),
err
);
0
}
};
Message::DirectoryChildren(path, children)
})
.await
.unwrap()
};
if let Err(err) = output.send(message).await {
log::warn!("failed to send directory children: {err}");
}
std::future::pending().await
},
)
},
));
}
for item in items { for item in items {
if item.thumbnail_opt.is_some() { if item.thumbnail_opt.is_some() {
// Skip items that already have a mime type and thumbnail // Skip items that already have a mime type and thumbnail
@ -7012,7 +7097,7 @@ impl Tab {
let path = path.clone(); let path = path.clone();
// Acquire semaphore permit // Acquire semaphore permit
_ = THUMB_SEMAPHORE.acquire().await; let _permit = THUMB_SEMAPHORE.acquire().await.unwrap();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let start = Instant::now(); let start = Instant::now();
@ -7316,6 +7401,19 @@ impl Tab {
const fn format_time(&self, time: SystemTime) -> FormatTime<'_> { const fn format_time(&self, time: SystemTime) -> FormatTime<'_> {
format_time(time, &self.date_time_formatter, &self.time_formatter) format_time(time, &self.date_time_formatter, &self.time_formatter)
} }
fn on_drag<'a>(&self, rect_opt: Option<Rectangle>) -> Option<Message> {
let rect = rect_opt?;
// We only want to publish a drag message if the overlapped items of the drag rect change,
// otherwise a view rebuild is triggered on every drag event.
let changed = self.items_opt.as_ref().is_some_and(|items| {
items.iter().any(|item| {
let overlaps = item.rect_opt.get().is_some_and(|r| r.intersects(&rect));
overlaps != item.overlaps_drag_rect
})
});
changed.then_some(Message::Drag(Some(rect)))
}
} }
pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> Option<Message> { pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: &Modifiers) -> Option<Message> {

View file

@ -175,3 +175,25 @@ pub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {
let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap(); let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
thumbnailer_cache.get(mime) thumbnailer_cache.get(mime)
} }
#[cfg(all(test, feature = "desktop"))]
mod tests {
use super::Thumbnailer;
use std::path::Path;
#[test]
fn command_expands_thumbnailer_arguments() {
let thumbnailer = Thumbnailer {
exec: "cosmic-files-thumbnailer %o --size %s %i".to_string(),
};
let command = thumbnailer
.command(Path::new("input.exe"), Path::new("output.png"), 128)
.expect("valid thumbnailer command");
assert_eq!(command.get_program(), "cosmic-files-thumbnailer");
assert_eq!(
command.get_args().collect::<Vec<_>>(),
["output.png", "--size", "128", "input.exe"]
);
}
}

View file

@ -2,6 +2,7 @@ use cosmic::widget;
use regex::Regex; use regex::Regex;
use std::collections::HashSet; use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::config::IconSizes; use crate::config::IconSizes;
use crate::tab::{Item, SearchItem}; use crate::tab::{Item, SearchItem};
@ -89,11 +90,12 @@ pub fn trash_item_path(item: &trash::TrashItem) -> Option<PathBuf> {
/// - Read `~/.local/share/Trash/info/folder.trashinfo` to get the original path /// - Read `~/.local/share/Trash/info/folder.trashinfo` to get the original path
/// - Compute: `<original_path>/sub/file.txt` /// - Compute: `<original_path>/sub/file.txt`
pub fn original_path_for_trash_child(p: &Path) -> Option<PathBuf> { pub fn original_path_for_trash_child(p: &Path) -> Option<PathBuf> {
let files = p.ancestors().find(|a| a.ends_with("files"))?; let files = trash_files_dir(p)?;
let root = files.parent()?; let root = files.parent()?;
let top = p.strip_prefix(files).ok()?.components().next()?; let top = p.strip_prefix(files).ok()?.components().next()?;
let info = let mut trashinfo_name = top.as_os_str().to_os_string();
std::fs::read_to_string(root.join("info").join(top).with_extension("trashinfo")).ok()?; trashinfo_name.push(".trashinfo");
let info = std::fs::read_to_string(root.join("info").join(trashinfo_name)).ok()?;
let orig = percent_decode(info.lines().find_map(|l| l.strip_prefix("Path="))?.trim())?; let orig = percent_decode(info.lines().find_map(|l| l.strip_prefix("Path="))?.trim())?;
let rel = p.strip_prefix(files.join(top)).ok()?; let rel = p.strip_prefix(files.join(top)).ok()?;
let mut result = PathBuf::from(&orig); let mut result = PathBuf::from(&orig);
@ -103,9 +105,29 @@ pub fn original_path_for_trash_child(p: &Path) -> Option<PathBuf> {
Some(result) Some(result)
} }
static TRASH_FOLDERS: LazyLock<HashSet<PathBuf>> = LazyLock::new(|| {
Trash::folders().unwrap_or_else(|e| {
log::warn!("failed to list trash folders: {}", e);
HashSet::new()
})
});
fn is_trash_root(root: &Path) -> bool {
if TRASH_FOLDERS.is_empty() {
root.join("info").is_dir()
} else {
TRASH_FOLDERS.contains(root)
}
}
fn trash_files_dir(path: &Path) -> Option<&Path> {
path.ancestors()
.find(|a| a.ends_with("files") && a.parent().is_some_and(is_trash_root))
}
/// Check whether a path is inside any trash `files/` directory. /// Check whether a path is inside any trash `files/` directory.
pub fn is_trash_path(path: &Path) -> bool { pub fn is_trash_path(path: &Path) -> bool {
path.ancestors().any(|a| a.ends_with("files")) trash_files_dir(path).is_some()
} }
pub struct Trash; pub struct Trash;