From 2926b919801f139e0b5725d661d3a5ecd64b1354 Mon Sep 17 00:00:00 2001 From: Xarvex <60973030+xarvex@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:05:40 -0400 Subject: [PATCH 01/19] chore(thumbs): prepare for pillow_heif removing AVIF support (#1065) * fix(thumb_renderer): prepare for pillow_heif removing AVIF support * fix(nix/package): add pillow-avif-plugin --- nix/package/default.nix | 2 ++ pyproject.toml | 3 ++- src/tagstudio/qt/widgets/thumb_renderer.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nix/package/default.nix b/nix/package/default.nix index 631a4f47..ba697e22 100644 --- a/nix/package/default.nix +++ b/nix/package/default.nix @@ -74,6 +74,7 @@ python3Packages.buildPythonApplication { pythonRelaxDeps = [ "numpy" "pillow" + "pillow-avif-plugin" "pillow-heif" "pillow-jxl-plugin" "pyside6" @@ -93,6 +94,7 @@ python3Packages.buildPythonApplication { numpy opencv-python pillow + pillow-avif-plugin pillow-heif pydantic pydub diff --git a/pyproject.toml b/pyproject.toml index 5b47e95e..2097cb9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ dependencies = [ "mutagen~=1.47", "numpy~=2.2", "opencv_python~=4.11", - "Pillow>=10.2,<=12.0", + "Pillow>=10.2,<=11", + "pillow-avif-plugin~=1.5", "pillow-heif~=0.22", "pillow-jxl-plugin~=1.3", "pydantic~=2.10", diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index 16d27b2e..42cbe9e8 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -16,6 +16,7 @@ from warnings import catch_warnings import cv2 import numpy as np +import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport] import rawpy import srctools import structlog @@ -33,7 +34,7 @@ from PIL import ( UnidentifiedImageError, ) from PIL.Image import DecompressionBombError -from pillow_heif import register_avif_opener, register_heif_opener +from pillow_heif import register_heif_opener from PySide6.QtCore import ( QBuffer, QFile, @@ -79,7 +80,6 @@ os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1" logger = structlog.get_logger(__name__) Image.MAX_IMAGE_PIXELS = None register_heif_opener() -register_avif_opener() try: import pillow_jxl # noqa: F401 # pyright: ignore[reportUnusedImport] From a9bdd93c64fd3d8372e45896022808a26bf2f17e Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:29:13 -0700 Subject: [PATCH 02/19] docs: update roadmap --- docs/updates/roadmap.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index 15fafefd..51856204 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -77,7 +77,7 @@ A detailed written specification for the TagStudio tag and/or library format. In - [ ] Lightbox View :material-chevron-triple-up:{ .priority-high title="High Priority" } - Similar to List View in concept, but displays one large preview that can cycle back/forth between entries. - [ ] Smaller thumbnails of immediate adjacent entries below :material-chevron-double-up:{ .priority-med title="Medium Priority" } -- [ ] Library Statistics Screen :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.5.4]** +- [x] Library Statistics Screen :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.5.4]** - [ ] Unified Library Health/Cleanup Screen :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.5.4]** - [x] Fix Unlinked Entries - [x] Fix Duplicate Files @@ -125,7 +125,7 @@ A detailed written specification for the TagStudio tag and/or library format. In - [x] Language - [x] Date and Time Format - [x] Theme - - [ ] Thumbnail Generation :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.5.4]** + - [x] Thumbnail Generation :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.5.4]** - [x] Configurable Page Size - [ ] Library Settings :material-chevron-triple-up:{ .priority-high title="High Priority" } - [ ] Stored in `.TagStudio` folder :material-chevron-triple-up:{ .priority-high title="High Priority" } @@ -147,6 +147,7 @@ Some form of official plugin support for TagStudio, likely with its own API that - [x] Per-Library Tags - [ ] Global Tags :material-chevron-triple-up:{ .priority-high title="High Priority" } - [ ] Multiple Root Directories :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]** + - [ ] Ability to store TagStudio library folder separate from library files :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]** - [ ] Automatic Entry Relinking :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.0]** - [ ] Detect Renames :material-chevron-triple-up:{ .priority-high title="High Priority" } - [ ] Detect Moves :material-chevron-triple-up:{ .priority-high title="High Priority" } @@ -168,7 +169,7 @@ Library representations of files or file-like objects. - [x] Text Boxes - [x] Datetimes **[v9.5.4]** - [ ] User-Titled Fields :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]** - - [ ] Removal of Deprecated Fields :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]** + - [ ] Removal of Deprecated Fields :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.0]** - [ ] Entry Groups :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.0]** - [ ] Non-exclusive; Entries can be in multiple groups :material-chevron-triple-up:{ .priority-high title="High Priority" } - [ ] Ability to number entries within group :material-chevron-triple-up:{ .priority-high title="High Priority" } From d8b058ac5aeeb5c63833fccad2e763d48b8fa55f Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:40:32 -0700 Subject: [PATCH 03/19] docs: update launch arguments --- docs/usage.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 7fb6c2ea..24de2ff0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -71,7 +71,9 @@ As of version 9.5, libraries are saved automatically as you go. To save a backup There are a handful of launch arguments you can pass to TagStudio via the command line or a desktop shortcut. -| Argument | Short | Description | -| ---------------------- | ----- | ---------------------------------------------------- | -| `--open ` | `-o` | Path to a TagStudio Library folder to open on start. | -| `--config-file ` | `-c` | Path to the TagStudio config file to load. | +| Argument | Short | Description | +| ------------------------ | ----- | ------------------------------------------------------ | +| `--cache-file ` | `-c` | Path to a TagStudio .ini or .plist cache file to use. | +| `--open ` | `-o` | Path to a TagStudio Library folder to open on start. | +| `--settings-file ` | `-s` | Path to a TagStudio .toml global settings file to use. | +| `--version` | `-v` | Displays TagStudio version information. | From 131c5df86b3a40010bfb7a90b039f520d7843dff Mon Sep 17 00:00:00 2001 From: Xarvex Date: Sat, 30 Aug 2025 12:16:22 -0500 Subject: [PATCH 04/19] fix(nix/package): ignore mutating test files, add new problematic tests --- nix/package/default.nix | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/nix/package/default.nix b/nix/package/default.nix index ba697e22..01b18b4c 100644 --- a/nix/package/default.nix +++ b/nix/package/default.nix @@ -110,34 +110,24 @@ python3Packages.buildPythonApplication { ] ++ lib.optional withJXLSupport pillow-jxl-plugin; + # These tests require modifications to a library, which does not work + # in a read-only environment. disabledTests = [ - # These tests require modifications to a library, which does not work - # in a read-only environment. - "test_build_tag_panel_add_alias_callback" - "test_build_tag_panel_add_aliases" - "test_build_tag_panel_add_sub_tag_callback" - "test_build_tag_panel_build_tag" - "test_build_tag_panel_remove_alias_callback" - "test_build_tag_panel_remove_subtag_callback" - "test_build_tag_panel_set_aliases" - "test_build_tag_panel_set_parent_tags" - "test_build_tag_panel_set_tag" + "test_badge_visual_state" + "test_browsing_state_update" + "test_flow_layout_happy_path" "test_json_migration" "test_library_migrations" - - "test_add_same_tag_to_selection_single" - "test_add_tag_to_selection_multiple" - "test_add_tag_to_selection_single" - "test_custom_tag_category" - "test_file_path_display" - "test_meta_tag_category" - "test_update_selection_empty" - "test_update_selection_empty" - "test_update_selection_multiple" - "test_update_selection_single" - - # This test requires modification of a configuration file. - "test_filepath_setting" + "test_update_tags" + ]; + disabledTestPaths = [ + "tests/qt/test_build_tag_panel.py" + "tests/qt/test_field_containers.py" + "tests/qt/test_file_path_options.py" + "tests/qt/test_preview_panel.py" + "tests/qt/test_tag_panel.py" + "tests/qt/test_tag_search_panel.py" + "tests/test_library.py" ]; meta = { From 9891caca35498a2db47818ff8ab3aaf2478567d8 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sat, 30 Aug 2025 15:34:52 -0700 Subject: [PATCH 05/19] fix: set generate_thumbs default to true --- src/tagstudio/core/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index 087e2dc1..a1d97c6a 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -44,7 +44,7 @@ class Theme(Enum): class GlobalSettings(BaseModel): language: str = Field(default="en") open_last_loaded_on_startup: bool = Field(default=True) - generate_thumbs: bool = Field(default=False) + generate_thumbs: bool = Field(default=True) autoplay: bool = Field(default=True) loop: bool = Field(default=True) show_filenames_in_grid: bool = Field(default=True) From dcf564e8c3dba0886e1503178041b0c96af64a08 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:00:39 -0700 Subject: [PATCH 06/19] fix: add loop cutoff to get_tag_categories() (#1075) --- .../qt/widgets/preview/field_containers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/tagstudio/qt/widgets/preview/field_containers.py b/src/tagstudio/qt/widgets/preview/field_containers.py index a23714cc..4bd7d03c 100644 --- a/src/tagstudio/qt/widgets/preview/field_containers.py +++ b/src/tagstudio/qt/widgets/preview/field_containers.py @@ -168,9 +168,11 @@ class FieldContainers(QWidget): "Character" -> "Johnny Bravo", "TV" -> Johnny Bravo" """ - hierarchy_tags = self.lib.get_tag_hierarchy(t.id for t in tags) + loop_cutoff = 1024 # Used for stopping the while loop + hierarchy_tags = self.lib.get_tag_hierarchy(t.id for t in tags) categories: dict[Tag | None, set[Tag]] = {None: set()} + for tag in hierarchy_tags.values(): if tag.is_category: categories[tag] = set() @@ -178,7 +180,15 @@ class FieldContainers(QWidget): tag = hierarchy_tags[tag.id] has_category_parent = False parent_tags = tag.parent_tags + + loop_counter = 0 while len(parent_tags) > 0: + # NOTE: This is for preventing infinite loops in the event a tag is parented + # to itself cyclically. + loop_counter += 1 + if loop_counter >= loop_cutoff: + break + grandparent_tags: set[Tag] = set() for parent_tag in parent_tags: if parent_tag in categories: @@ -186,6 +196,7 @@ class FieldContainers(QWidget): has_category_parent = True grandparent_tags.update(parent_tag.parent_tags) parent_tags = grandparent_tags + if tag.is_category: categories[tag].add(tag) elif not has_category_parent: From a9a1470a0880c93614a3384df1c04cc50af4a983 Mon Sep 17 00:00:00 2001 From: purpletennisball <212221402+purpletennisball@users.noreply.github.com> Date: Sun, 31 Aug 2025 19:38:01 -0400 Subject: [PATCH 07/19] fix: parent tags in tag editor are uneditable (#1073) --- src/tagstudio/qt/modals/build_tag.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/qt/modals/build_tag.py b/src/tagstudio/qt/modals/build_tag.py index d1a99b13..9dd077e4 100644 --- a/src/tagstudio/qt/modals/build_tag.py +++ b/src/tagstudio/qt/modals/build_tag.py @@ -30,7 +30,7 @@ from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag, TagColorGroup from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color from tagstudio.qt.modals.tag_color_selection import TagColorSelection -from tagstudio.qt.modals.tag_search import TagSearchModal +from tagstudio.qt.modals.tag_search import TagSearchModal, TagSearchPanel from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget from tagstudio.qt.widgets.tag import ( @@ -385,10 +385,11 @@ class BuildTagPanel(PanelWidget): tag_widget = TagWidget( tag, library=self.lib, - has_edit=False, + has_edit=True, has_remove=True, ) tag_widget.on_remove.connect(lambda t=parent_id: self.remove_parent_tag_callback(t)) + tag_widget.on_edit.connect(lambda t=tag: TagSearchPanel(library=self.lib).edit_tag(t)) row.addWidget(tag_widget) # Add Disambiguation Tag Button From 7a7e1cc4bdf64777d2004e4bcb6bb4310a7d0015 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 1 Sep 2025 01:49:53 +0200 Subject: [PATCH 08/19] translations: update from Hosted Weblate (#1071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Hungarian) Currently translated at 100.0% (330 of 330 strings) Co-authored-by: Szíjártó Levente Pál Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/ Translation: TagStudio/Strings * Translated using Weblate (French) Currently translated at 100.0% (330 of 330 strings) Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/ Translation: TagStudio/Strings --------- Co-authored-by: Szíjártó Levente Pál Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com> --- src/tagstudio/resources/translations/fr.json | 13 ++++++++++--- src/tagstudio/resources/translations/hu.json | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/tagstudio/resources/translations/fr.json b/src/tagstudio/resources/translations/fr.json index b85dec2c..7deacf58 100644 --- a/src/tagstudio/resources/translations/fr.json +++ b/src/tagstudio/resources/translations/fr.json @@ -136,6 +136,7 @@ "home.thumbnail_size.medium": "Miniatures Moyennes", "home.thumbnail_size.mini": "Mini Miniatures", "home.thumbnail_size.small": "Petites Miniatures", + "ignore.open_file": "Afficher le fichier \"{ts_ignore}\" sur le Disque", "json_migration.checking_for_parity": "Vérification de la Parité...", "json_migration.creating_database_tables": "Création des Tables de Base de Données SQL...", "json_migration.description": "
Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie ne sera utilisée que si vous cliquez sur \"Terminer la migration\".

Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"(!)\" à côté d'elles.
Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.
", @@ -161,9 +162,6 @@ "json_migration.title.new_lib": "

Bibliothèque v9.5+

", "json_migration.title.old_lib": "

Bibliothèque v9.4

", "landing.open_create_library": "Ouvrir/Créer une Bibliothèque {shortcut}", - "library_info.stats.entries": "Entrées :", - "library_info.stats.fields": "Champs :", - "library_info.stats.tags": "Tags :", "library.field.add": "Ajouter un Champ", "library.field.confirm_remove": "Êtes-vous sûr de vouloir supprimer le champ \"{name}\"?", "library.field.mixed_data": "Données Mélangées", @@ -175,6 +173,14 @@ "library.refresh.scanning_preparing": "Recherche de Nouveaux Fichiers dans les Dossiers...\nPréparation...", "library.refresh.title": "Rafraîchissement des Dossiers", "library.scan_library.title": "Balayage de la Bibliothèque", + "library_info.stats": "Statistiques", + "library_info.stats.colors": "Couleurs de Tag :", + "library_info.stats.entries": "Entrées :", + "library_info.stats.fields": "Champs :", + "library_info.stats.macros": "Macros :", + "library_info.stats.namespaces": "Namespaces :", + "library_info.stats.tags": "Tags :", + "library_info.title": "Bibliothèque '{library_dir}'", "library_object.name": "Nom", "library_object.name_required": "Nom (Requis)", "library_object.slug": "Identifiant unique", @@ -214,6 +220,7 @@ "menu.view": "&Vues", "menu.view.decrease_thumbnail_size": "Rétrécir les vignettes", "menu.view.increase_thumbnail_size": "Agrandir les vignettes", + "menu.view.library_info": "Bibliothèque &Information", "menu.window": "Fenêtre", "namespace.create.description": "Les namespaces sont utilisés par TagStudio pour séparer les groupes d'éléments tels que les Tags et les couleurs de manière à faciliter leur exportation et leur partage. Les namespace avec des noms commençant par « tagstudio » sont réservés par TagStudio pour un usage interne.", "namespace.create.description_color": "Les tags utilise les namespace pour regrouper plusieurs couleurs. Toutes les couleurs personnalisées doivent être ajoutées à un namespace.", diff --git a/src/tagstudio/resources/translations/hu.json b/src/tagstudio/resources/translations/hu.json index b7a092b2..bd0d307c 100644 --- a/src/tagstudio/resources/translations/hu.json +++ b/src/tagstudio/resources/translations/hu.json @@ -136,6 +136,7 @@ "home.thumbnail_size.medium": "Közepes miniatűrök", "home.thumbnail_size.mini": "Pici miniatűrök", "home.thumbnail_size.small": "Kicsi miniatűrök", + "ignore.open_file": "„{ts_ignore}” fájl megjelenítése a lemezen", "json_migration.checking_for_parity": "Paritás ellenőrzése folyamatban…", "json_migration.creating_database_tables": "SQL-adatbázis táblázatainak létrehozása folyamatban…", "json_migration.description": "
A könyvtárátalakítási folyamat megkezdése és az eredmény előnézete. Az új könyvtár az „Átalakítás befejezése” gomb megnyomásáig nem lesz használatba véve.

A könyvtár adatai változatlanok maradnak vagy egy „Egységesítve” címkével lesznek felruházva. A nem egyező adatok vörösen lesznek megjelenítve és egy „(!)” szimbólummal lesznek ellátva.
Ez a folyamat nagyobb könyvtárak esetén akár több percig is eltarthat.
", @@ -161,9 +162,6 @@ "json_migration.title.new_lib": "

9.5 és afölötti könyvtár

", "json_migration.title.old_lib": "

9.4-es könyvtár

", "landing.open_create_library": "Könyvtár meg&nyitása/létrehozása {shortcut}", - "library_info.stats.entries": "Elemek:", - "library_info.stats.fields": "Mezők:", - "library_info.stats.tags": "Címkék:", "library.field.add": "Új mező", "library.field.confirm_remove": "Biztosan el akarja távolítani a(z) „{name}”-mezőt?", "library.field.mixed_data": "Kevert adatok", @@ -175,6 +173,14 @@ "library.refresh.scanning_preparing": "Új fájlok keresése a mappákban…\nElőkészítés…", "library.refresh.title": "Könyvtárak frissítése", "library.scan_library.title": "Könyvtár vizsgálata", + "library_info.stats": "Statisztikák", + "library_info.stats.colors": "Címkeszínek:", + "library_info.stats.entries": "Elemek:", + "library_info.stats.fields": "Mezők:", + "library_info.stats.macros": "Makrók:", + "library_info.stats.namespaces": "Névterek:", + "library_info.stats.tags": "Címkék:", + "library_info.title": "„{library_dir}” könyvtár", "library_object.name": "Megnevezés", "library_object.name_required": "Megnevezés (kötelező)", "library_object.slug": "Azonosító-helyőrző", @@ -214,6 +220,7 @@ "menu.view": "&Nézet", "menu.view.decrease_thumbnail_size": "Indexkép méretének csökkentése", "menu.view.increase_thumbnail_size": "Indexkép méretének növelése", + "menu.view.library_info": "&Könyvtárinformáció", "menu.window": "&Ablak", "namespace.create.description": "A TagStudio névterekkel különíti el az adatcsoportokat, mint a címkék és a színek, így azok könnyen exportálhatóak és megoszthatóak. A „tagstudio”-val kezdődő névterek belső használatra vannak lefoglalva.", "namespace.create.description_color": "Minden szín névterekbe van foglalva, amelyek színpalettaként viselkednek. Minden egyéni színt névtérbe kell foglalni.", From 2f4b72fd4d33f07b616b1f5e03766243dd2b15b5 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:53:56 -0700 Subject: [PATCH 09/19] feat: add library cleanup screen and 'fix ignored files' window (#1070) * feat(ui): add LibraryInfoWindow with statistics * feat: add library cleanup screen * fix: missing resource * tests: add basic test for resource_manager.py * feat: remove ignored files in bulk * feat: open backups folder from library info window * refactor: rename unlinked+ignored modal files * refactor: sort en.json --- .../core/library/alchemy/constants.py | 3 + src/tagstudio/core/library/alchemy/library.py | 20 +- src/tagstudio/core/ts_core.py | 2 +- src/tagstudio/core/utils/ignored_registry.py | 51 +++ src/tagstudio/core/utils/refresh_dir.py | 7 +- ...{missing_files.py => unlinked_registry.py} | 58 +-- .../fix_ignored_modal_controller.py | 94 +++++ .../widgets/library_info_window_controller.py | 122 +++++- src/tagstudio/qt/main_window.py | 12 +- src/tagstudio/qt/modals/fix_dupes.py | 3 +- src/tagstudio/qt/modals/fix_unlinked.py | 99 +++-- ...or_entities.py => mirror_entries_modal.py} | 0 ...nk_unlinked.py => relink_entries_modal.py} | 11 +- ...te_unlinked.py => remove_ignored_modal.py} | 30 +- .../qt/modals/remove_unlinked_modal.py | 119 ++++++ src/tagstudio/qt/resources.json | 280 +++++++------- src/tagstudio/qt/ts_qt.py | 16 +- .../qt/view/widgets/fix_ignored_modal_view.py | 67 ++++ .../view/widgets/library_info_window_view.py | 366 ++++++++++++++++-- src/tagstudio/qt/widgets/thumb_renderer.py | 5 +- .../resources/qt/images/dupe_file_stat.png | Bin 0 -> 2973 bytes .../resources/qt/images/ignored_stat.png | Bin 0 -> 4184 bytes .../resources/qt/images/unlinked_stat.png | Bin 0 -> 6287 bytes src/tagstudio/resources/translations/de.json | 13 +- src/tagstudio/resources/translations/en.json | 57 ++- src/tagstudio/resources/translations/es.json | 13 +- src/tagstudio/resources/translations/fil.json | 13 +- src/tagstudio/resources/translations/fr.json | 13 +- src/tagstudio/resources/translations/hu.json | 13 +- src/tagstudio/resources/translations/ja.json | 13 +- .../resources/translations/nb_NO.json | 13 +- src/tagstudio/resources/translations/pl.json | 13 +- src/tagstudio/resources/translations/pt.json | 13 +- .../resources/translations/pt_BR.json | 13 +- src/tagstudio/resources/translations/qpv.json | 13 +- src/tagstudio/resources/translations/ru.json | 13 +- src/tagstudio/resources/translations/sv.json | 8 +- src/tagstudio/resources/translations/ta.json | 13 +- src/tagstudio/resources/translations/tok.json | 13 +- src/tagstudio/resources/translations/tr.json | 13 +- .../resources/translations/zh_Hans.json | 13 +- .../resources/translations/zh_Hant.json | 13 +- tests/macros/test_missing_files.py | 8 +- tests/qt/test_resource_manager.py | 19 + tests/test_db_migrations.py | 7 +- 45 files changed, 1224 insertions(+), 461 deletions(-) create mode 100644 src/tagstudio/core/utils/ignored_registry.py rename src/tagstudio/core/utils/{missing_files.py => unlinked_registry.py} (56%) create mode 100644 src/tagstudio/qt/controller/fix_ignored_modal_controller.py rename src/tagstudio/qt/modals/{mirror_entities.py => mirror_entries_modal.py} (100%) rename src/tagstudio/qt/modals/{relink_unlinked.py => relink_entries_modal.py} (78%) rename src/tagstudio/qt/modals/{delete_unlinked.py => remove_ignored_modal.py} (79%) create mode 100644 src/tagstudio/qt/modals/remove_unlinked_modal.py create mode 100644 src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py create mode 100644 src/tagstudio/resources/qt/images/dupe_file_stat.png create mode 100644 src/tagstudio/resources/qt/images/ignored_stat.png create mode 100644 src/tagstudio/resources/qt/images/unlinked_stat.png create mode 100644 tests/qt/test_resource_manager.py diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index 4103e963..26ce1c83 100644 --- a/src/tagstudio/core/library/alchemy/constants.py +++ b/src/tagstudio/core/library/alchemy/constants.py @@ -5,6 +5,9 @@ from sqlalchemy import text +SQL_FILENAME: str = "ts_library.sqlite" +JSON_FILENAME: str = "ts_library.json" + DB_VERSION_LEGACY_KEY: str = "DB_VERSION" DB_VERSION_CURRENT_KEY: str = "CURRENT" DB_VERSION_INITIAL_KEY: str = "INITIAL" diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index f936bfbb..c4becc3a 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -67,6 +67,8 @@ from tagstudio.core.library.alchemy.constants import ( DB_VERSION_CURRENT_KEY, DB_VERSION_INITIAL_KEY, DB_VERSION_LEGACY_KEY, + JSON_FILENAME, + SQL_FILENAME, TAG_CHILDREN_QUERY, ) from tagstudio.core.library.alchemy.db import make_tables @@ -213,8 +215,11 @@ class Library: folder: Folder | None included_files: set[Path] = set() - SQL_FILENAME: str = "ts_library.sqlite" - JSON_FILENAME: str = "ts_library.json" + def __init__(self) -> None: + self.dupe_entries_count: int = -1 # NOTE: For internal management. + self.dupe_files_count: int = -1 + self.ignored_entries_count: int = -1 + self.unlinked_entries_count: int = -1 def close(self): if self.engine: @@ -224,6 +229,11 @@ class Library: self.folder = None self.included_files = set() + self.dupe_entries_count = -1 + self.dupe_files_count = -1 + self.ignored_entries_count = -1 + self.unlinked_entries_count = -1 + def migrate_json_to_sqlite(self, json_lib: JsonLibrary): """Migrate JSON library data to the SQLite database.""" logger.info("Starting Library Conversion...") @@ -340,10 +350,10 @@ class Library: is_new = True return self.open_sqlite_library(library_dir, is_new) else: - self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME + self.storage_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME assert isinstance(self.storage_path, Path) if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()): - json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME + json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME if json_path.exists(): return LibraryStatus( success=False, @@ -1513,7 +1523,7 @@ class Library: target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename shutil.copy2( - self.library_dir / TS_FOLDER_NAME / self.SQL_FILENAME, + self.library_dir / TS_FOLDER_NAME / SQL_FILENAME, target_path, ) diff --git a/src/tagstudio/core/ts_core.py b/src/tagstudio/core/ts_core.py index 05958bda..97ead78c 100644 --- a/src/tagstudio/core/ts_core.py +++ b/src/tagstudio/core/ts_core.py @@ -11,7 +11,7 @@ from tagstudio.core.constants import TS_FOLDER_NAME from tagstudio.core.library.alchemy.fields import _FieldID from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry -from tagstudio.core.utils.missing_files import logger +from tagstudio.core.utils.unlinked_registry import logger class TagStudioCore: diff --git a/src/tagstudio/core/utils/ignored_registry.py b/src/tagstudio/core/utils/ignored_registry.py new file mode 100644 index 00000000..a864e248 --- /dev/null +++ b/src/tagstudio/core/utils/ignored_registry.py @@ -0,0 +1,51 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from collections.abc import Iterator +from dataclasses import dataclass, field +from pathlib import Path + +import structlog + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.library.ignore import Ignore +from tagstudio.core.utils.types import unwrap + +logger = structlog.get_logger(__name__) + + +@dataclass +class IgnoredRegistry: + """State tracker for ignored entries.""" + + lib: Library + ignored_entries: list[Entry] = field(default_factory=list) + + @property + def ignored_count(self) -> int: + return len(self.ignored_entries) + + def reset(self): + self.ignored_entries.clear() + + def refresh_ignored_entries(self) -> Iterator[int]: + """Track the number of entries that would otherwise be ignored by the current rules.""" + logger.info("[IgnoredRegistry] Refreshing ignored entries...") + + self.ignored_entries = [] + library_dir: Path = unwrap(self.lib.library_dir) + + for i, entry in enumerate(self.lib.all_entries()): + if not Ignore.compiled_patterns: + # If the compiled_patterns has malfunctioned, don't consider that a false positive + yield i + elif Ignore.compiled_patterns.match(library_dir / entry.path): + self.ignored_entries.append(entry) + yield i + + def remove_ignored_entries(self) -> None: + self.lib.remove_entries(list(map(lambda ignored: ignored.id, self.ignored_entries))) + self.ignored_entries = [] diff --git a/src/tagstudio/core/utils/refresh_dir.py b/src/tagstudio/core/utils/refresh_dir.py index a7a8d064..beec03b2 100644 --- a/src/tagstudio/core/utils/refresh_dir.py +++ b/src/tagstudio/core/utils/refresh_dir.py @@ -1,3 +1,8 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + import shutil from collections.abc import Iterator from dataclasses import dataclass, field @@ -11,7 +16,7 @@ from wcmatch import pathlib from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob -from tagstudio.qt.helpers.silent_popen import silent_run +from tagstudio.qt.helpers.silent_popen import silent_run # pyright: ignore logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/core/utils/missing_files.py b/src/tagstudio/core/utils/unlinked_registry.py similarity index 56% rename from src/tagstudio/core/utils/missing_files.py rename to src/tagstudio/core/utils/unlinked_registry.py index 37fc0631..20bab624 100644 --- a/src/tagstudio/core/utils/missing_files.py +++ b/src/tagstudio/core/utils/unlinked_registry.py @@ -14,34 +14,37 @@ logger = structlog.get_logger() @dataclass -class MissingRegistry: - """State tracker for unlinked and moved files.""" +class UnlinkedRegistry: + """State tracker for unlinked entries.""" - library: Library + lib: Library files_fixed_count: int = 0 - missing_file_entries: list[Entry] = field(default_factory=list) + unlinked_entries: list[Entry] = field(default_factory=list) @property - def missing_file_entries_count(self) -> int: - return len(self.missing_file_entries) + def unlinked_entries_count(self) -> int: + return len(self.unlinked_entries) - def refresh_missing_files(self) -> Iterator[int]: + def reset(self): + self.unlinked_entries.clear() + + def refresh_unlinked_files(self) -> Iterator[int]: """Track the number of entries that point to an invalid filepath.""" - logger.info("[refresh_missing_files] Refreshing missing files...") + logger.info("[UnlinkedRegistry] Refreshing unlinked files...") - self.missing_file_entries = [] - for i, entry in enumerate(self.library.all_entries()): - full_path = unwrap(self.library.library_dir) / entry.path + self.unlinked_entries = [] + for i, entry in enumerate(self.lib.all_entries()): + full_path = unwrap(self.lib.library_dir) / entry.path if not full_path.exists() or not full_path.is_file(): - self.missing_file_entries.append(entry) + self.unlinked_entries.append(entry) yield i - def match_missing_file_entry(self, match_entry: Entry) -> list[Path]: + def match_unlinked_file_entry(self, match_entry: Entry) -> list[Path]: """Try and match unlinked file entries with matching results in the library directory. Works if files were just moved to different subfolders and don't have duplicate names. """ - library_dir = unwrap(self.library.library_dir) + library_dir = unwrap(self.lib.library_dir) matches: list[Path] = [] ignore_patterns = Ignore.get_patterns(library_dir) @@ -56,26 +59,26 @@ class MissingRegistry: new_path = Path(path).relative_to(library_dir) matches.append(new_path) - logger.info("[MissingRegistry] Matches", matches=matches) + logger.info("[UnlinkedRegistry] Matches", matches=matches) return matches def fix_unlinked_entries(self) -> Iterator[int]: """Attempt to fix unlinked file entries by finding a match in the library directory.""" self.files_fixed_count = 0 matched_entries: list[Entry] = [] - for i, entry in enumerate(self.missing_file_entries): - item_matches = self.match_missing_file_entry(entry) + for i, entry in enumerate(self.unlinked_entries): + item_matches = self.match_unlinked_file_entry(entry) if len(item_matches) == 1: logger.info( - "[fix_unlinked_entries]", + "[UnlinkedRegistry]", entry=entry.path.as_posix(), item_matches=item_matches[0].as_posix(), ) - if not self.library.update_entry_path(entry.id, item_matches[0]): + if not self.lib.update_entry_path(entry.id, item_matches[0]): try: - match = unwrap(self.library.get_entry_full_by_path(item_matches[0])) - entry_full = unwrap(self.library.get_entry_full(entry.id)) - self.library.merge_entries(entry_full, match) + match = unwrap(self.lib.get_entry_full_by_path(item_matches[0])) + entry_full = unwrap(self.lib.get_entry_full(entry.id)) + self.lib.merge_entries(entry_full, match) except AttributeError: continue self.files_fixed_count += 1 @@ -83,11 +86,8 @@ class MissingRegistry: yield i for entry in matched_entries: - self.missing_file_entries.remove(entry) + self.unlinked_entries.remove(entry) - def execute_deletion(self) -> None: - self.library.remove_entries( - list(map(lambda missing: missing.id, self.missing_file_entries)) - ) - - self.missing_file_entries = [] + def remove_unlinked_entries(self) -> None: + self.lib.remove_entries(list(map(lambda unlinked: unlinked.id, self.unlinked_entries))) + self.unlinked_entries = [] diff --git a/src/tagstudio/qt/controller/fix_ignored_modal_controller.py b/src/tagstudio/qt/controller/fix_ignored_modal_controller.py new file mode 100644 index 00000000..324fab44 --- /dev/null +++ b/src/tagstudio/qt/controller/fix_ignored_modal_controller.py @@ -0,0 +1,94 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import TYPE_CHECKING, override + +import structlog +from PySide6 import QtGui + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.ignored_registry import IgnoredRegistry +from tagstudio.qt.modals.remove_ignored_modal import RemoveIgnoredModal +from tagstudio.qt.translations import Translations +from tagstudio.qt.view.widgets.fix_ignored_modal_view import FixIgnoredEntriesModalView +from tagstudio.qt.widgets.progress import ProgressWidget + +# Only import for type checking/autocompletion, will not be imported at runtime. +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + +logger = structlog.get_logger(__name__) + + +class FixIgnoredEntriesModal(FixIgnoredEntriesModalView): + def __init__(self, library: "Library", driver: "QtDriver"): + super().__init__(library, driver) + self.tracker = IgnoredRegistry(self.lib) + + self.remove_modal = RemoveIgnoredModal(self.driver, self.tracker) + self.remove_modal.done.connect( + lambda: ( + self.update_ignored_count(), + self.driver.update_browsing_state(), + self.driver.library_info_window.update_cleanup(), + self.refresh_ignored(), + ) + ) + + self.refresh_ignored_button.clicked.connect(self.refresh_ignored) + self.remove_button.clicked.connect(self.remove_modal.show) + self.done_button.clicked.connect(self.hide) + + self.update_ignored_count() + + def refresh_ignored(self): + pw = ProgressWidget( + cancel_button_text=None, + minimum=0, + maximum=self.lib.entries_count, + ) + pw.setWindowTitle(Translations["library.scan_library.title"]) + pw.update_label(Translations["entries.ignored.scanning"]) + + def update_driver_widgets(): + if ( + hasattr(self.driver, "library_info_window") + and self.driver.library_info_window.isVisible() + ): + self.driver.library_info_window.update_cleanup() + + pw.from_iterable_function( + self.tracker.refresh_ignored_entries, + None, + self.set_ignored_count, + self.update_ignored_count, + self.remove_modal.refresh_list, + update_driver_widgets, + ) + + def set_ignored_count(self): + """Sets the ignored_entries_count in the Library to the tracker's value.""" + self.lib.ignored_entries_count = self.tracker.ignored_count + + def update_ignored_count(self): + """Updates the UI to reflect the Library's current ignored_entries_count.""" + # Indicates that the library is new compared to the last update. + # NOTE: Make sure set_ignored_count() is called before this! + if self.tracker.ignored_count > 0 and self.lib.ignored_entries_count < 0: + self.tracker.reset() + + count: int = self.lib.ignored_entries_count + + self.remove_button.setDisabled(count < 1) + + count_text: str = Translations.format( + "entries.ignored.ignored_count", count=count if count >= 0 else "—" + ) + self.ignored_count_label.setText(f"

{count_text}

") + + @override + def showEvent(self, event: QtGui.QShowEvent) -> None: # type: ignore + self.update_ignored_count() + return super().showEvent(event) diff --git a/src/tagstudio/qt/controller/widgets/library_info_window_controller.py b/src/tagstudio/qt/controller/widgets/library_info_window_controller.py index db1bb0a4..d46164db 100644 --- a/src/tagstudio/qt/controller/widgets/library_info_window_controller.py +++ b/src/tagstudio/qt/controller/widgets/library_info_window_controller.py @@ -1,13 +1,24 @@ # Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import os +from pathlib import Path +from typing import TYPE_CHECKING, override +from warnings import catch_warnings - -from typing import TYPE_CHECKING - +import structlog +from humanfriendly import format_size # pyright: ignore[reportUnknownVariableType] from PySide6 import QtGui +from tagstudio.core.constants import BACKUP_FOLDER_NAME, TS_FOLDER_NAME +from tagstudio.core.library.alchemy.constants import ( + DB_VERSION, + DB_VERSION_CURRENT_KEY, + JSON_FILENAME, +) from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.types import unwrap +from tagstudio.qt.helpers import file_opener from tagstudio.qt.translations import Translations from tagstudio.qt.view.widgets.library_info_window_view import LibraryInfoWindowView @@ -15,17 +26,33 @@ from tagstudio.qt.view.widgets.library_info_window_view import LibraryInfoWindow if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver +logger = structlog.get_logger(__name__) + class LibraryInfoWindow(LibraryInfoWindowView): def __init__(self, library: "Library", driver: "QtDriver"): super().__init__(library, driver) + # Statistics Buttons self.manage_tags_button.clicked.connect( self.driver.main_window.menu_bar.tag_manager_action.trigger ) self.manage_colors_button.clicked.connect( self.driver.main_window.menu_bar.color_manager_action.trigger ) + + # Cleanup Buttons + self.fix_unlinked_entries.clicked.connect( + self.driver.main_window.menu_bar.fix_unlinked_entries_action.trigger + ) + self.fix_ignored_entries.clicked.connect( + self.driver.main_window.menu_bar.fix_ignored_entries_action.trigger + ) + self.fix_dupe_files.clicked.connect( + self.driver.main_window.menu_bar.fix_dupe_files_action.trigger + ) + + # General Buttons self.close_button.clicked.connect(lambda: self.close()) def update_title(self): @@ -47,6 +74,93 @@ class LibraryInfoWindow(LibraryInfoWindowView): self.macros_count_label.setText("1") # TODO: Implement macros system - def showEvent(self, event: QtGui.QShowEvent): # noqa N802 + def update_cleanup(self): + # Unlinked Entries + unlinked_count: str = ( + str(self.lib.unlinked_entries_count) if self.lib.unlinked_entries_count >= 0 else "—" + ) + self.unlinked_count_label.setText(f"{unlinked_count}") + + # Ignored Entries + ignored_count: str = ( + str(self.lib.ignored_entries_count) if self.lib.ignored_entries_count >= 0 else "—" + ) + self.ignored_count_label.setText(f"{ignored_count}") + + # Duplicate Files + dupe_files_count: str = ( + str(self.lib.dupe_files_count) if self.lib.dupe_files_count >= 0 else "—" + ) + self.dupe_files_count_label.setText(f"{dupe_files_count}") + + # Legacy JSON Library Present + json_library_text: str = ( + Translations["generic.yes"] + if self.__is_json_library_present + else Translations["generic.no"] + ) + self.legacy_json_status_label.setText(f"{json_library_text}") + + # Backups + self.backups_count_label.setText( + f"{self.__backups_count} ({format_size(self.__backups_size)})" + ) + + # Buttons + with catch_warnings(record=True): + self.view_legacy_json_file.clicked.disconnect() + self.open_backups_folder.clicked.disconnect() + + if self.__is_json_library_present: + self.view_legacy_json_file.setEnabled(True) + self.view_legacy_json_file.clicked.connect( + lambda: file_opener.open_file( + unwrap(self.lib.library_dir) / TS_FOLDER_NAME / JSON_FILENAME, file_manager=True + ) + ) + else: + self.view_legacy_json_file.setEnabled(False) + + self.open_backups_folder.clicked.connect( + lambda: file_opener.open_file( + unwrap(self.lib.library_dir) / TS_FOLDER_NAME / BACKUP_FOLDER_NAME + ) + ) + + def update_version(self): + version_text: str = f"{self.lib.get_version(DB_VERSION_CURRENT_KEY)} / {DB_VERSION}" + self.version_label.setText( + Translations.format("library_info.version", version=version_text) + ) + + def refresh(self): self.update_title() self.update_stats() + self.update_cleanup() + self.update_version() + + @property + def __is_json_library_present(self): + json_path = unwrap(self.lib.library_dir) / TS_FOLDER_NAME / JSON_FILENAME + return json_path.exists() + + @property + def __backups_count(self): + backups_path = unwrap(self.lib.library_dir) / TS_FOLDER_NAME / BACKUP_FOLDER_NAME + return len(os.listdir(backups_path)) + + @property + def __backups_size(self): + backups_path = unwrap(self.lib.library_dir) / TS_FOLDER_NAME / BACKUP_FOLDER_NAME + size: int = 0 + + for f in backups_path.glob("*"): + if not f.is_dir() and f.exists(): + size += Path(f).stat().st_size + + return size + + @override + def showEvent(self, event: QtGui.QShowEvent): # type: ignore + self.refresh() + return super().showEvent(event) diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index 5da072ca..171cc3fd 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -80,6 +80,7 @@ class MainMenuBar(QMenuBar): tools_menu: QMenu fix_unlinked_entries_action: QAction + fix_ignored_entries_action: QAction fix_dupe_files_action: QAction clear_thumb_cache_action: QAction @@ -349,6 +350,13 @@ class MainMenuBar(QMenuBar): self.fix_unlinked_entries_action.setEnabled(False) self.tools_menu.addAction(self.fix_unlinked_entries_action) + # Fix Ignored Entries + self.fix_ignored_entries_action = QAction( + Translations["menu.tools.fix_ignored_entries"], self + ) + self.fix_ignored_entries_action.setEnabled(False) + self.tools_menu.addAction(self.fix_ignored_entries_action) + # Fix Duplicate Files self.fix_dupe_files_action = QAction(Translations["menu.tools.fix_duplicate_files"], self) self.fix_dupe_files_action.setEnabled(False) @@ -510,11 +518,8 @@ class MainWindow(QMainWindow): self.central_layout.setObjectName("central_layout") self.setup_search_bar() - self.setup_extra_input_bar() - self.setup_content(driver) - self.setCentralWidget(self.central_widget) def setup_search_bar(self): @@ -618,7 +623,6 @@ class MainWindow(QMainWindow): self.content_splitter.setHandleWidth(12) self.setup_entry_list(driver) - self.setup_preview_panel(driver) self.content_splitter.setStretchFactor(0, 1) diff --git a/src/tagstudio/qt/modals/fix_dupes.py b/src/tagstudio/qt/modals/fix_dupes.py index be8fb3de..c2b141b3 100644 --- a/src/tagstudio/qt/modals/fix_dupes.py +++ b/src/tagstudio/qt/modals/fix_dupes.py @@ -18,7 +18,7 @@ from PySide6.QtWidgets import ( from tagstudio.core.library.alchemy.library import Library from tagstudio.core.utils.dupe_files import DupeRegistry -from tagstudio.qt.modals.mirror_entities import MirrorEntriesModal +from tagstudio.qt.modals.mirror_entries_modal import MirrorEntriesModal from tagstudio.qt.translations import Translations # Only import for type checking/autocompletion, will not be imported at runtime. @@ -26,6 +26,7 @@ if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver +# TODO: Break up into MVC classes, similar to fix_ignored_modal class FixDupeFilesModal(QWidget): def __init__(self, library: "Library", driver: "QtDriver"): super().__init__() diff --git a/src/tagstudio/qt/modals/fix_unlinked.py b/src/tagstudio/qt/modals/fix_unlinked.py index 89eced5f..0e92b153 100644 --- a/src/tagstudio/qt/modals/fix_unlinked.py +++ b/src/tagstudio/qt/modals/fix_unlinked.py @@ -10,10 +10,10 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from tagstudio.core.library.alchemy.library import Library -from tagstudio.core.utils.missing_files import MissingRegistry -from tagstudio.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal +from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry from tagstudio.qt.modals.merge_dupe_entries import MergeDuplicateEntries -from tagstudio.qt.modals.relink_unlinked import RelinkUnlinkedEntries +from tagstudio.qt.modals.relink_entries_modal import RelinkUnlinkedEntries +from tagstudio.qt.modals.remove_unlinked_modal import RemoveUnlinkedEntriesModal from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.progress import ProgressWidget @@ -22,15 +22,16 @@ if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver +# TODO: Break up into MVC classes, similar to fix_ignored_modal class FixUnlinkedEntriesModal(QWidget): def __init__(self, library: "Library", driver: "QtDriver"): super().__init__() self.lib = library self.driver = driver - self.tracker = MissingRegistry(library=self.lib) + self.tracker = UnlinkedRegistry(lib=self.lib) - self.missing_count = -1 + self.unlinked_count = -1 self.dupe_count = -1 self.setWindowTitle(Translations["entries.unlinked.title"]) self.setWindowModality(Qt.WindowModality.ApplicationModal) @@ -43,18 +44,16 @@ class FixUnlinkedEntriesModal(QWidget): self.unlinked_desc_widget.setWordWrap(True) self.unlinked_desc_widget.setStyleSheet("text-align:left;") - self.missing_count_label = QLabel() - self.missing_count_label.setObjectName("missingCountLabel") - self.missing_count_label.setStyleSheet("font-weight:bold;font-size:14px;") - self.missing_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.unlinked_count_label = QLabel() + self.unlinked_count_label.setObjectName("unlinkedCountLabel") + self.unlinked_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.dupe_count_label = QLabel() self.dupe_count_label.setObjectName("dupeCountLabel") - self.dupe_count_label.setStyleSheet("font-weight:bold;font-size:14px;") self.dupe_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.refresh_unlinked_button = QPushButton(Translations["entries.unlinked.refresh_all"]) - self.refresh_unlinked_button.clicked.connect(self.refresh_missing_files) + self.refresh_unlinked_button = QPushButton(Translations["entries.generic.refresh_alt"]) + self.refresh_unlinked_button.clicked.connect(self.refresh_unlinked) self.merge_class = MergeDuplicateEntries(self.lib, self.driver) self.relink_class = RelinkUnlinkedEntries(self.tracker) @@ -64,7 +63,7 @@ class FixUnlinkedEntriesModal(QWidget): # refresh the grid lambda: ( self.driver.update_browsing_state(), - self.refresh_missing_files(), + self.refresh_unlinked(), ) ) self.search_button.clicked.connect(self.relink_class.repair_entries) @@ -72,16 +71,17 @@ class FixUnlinkedEntriesModal(QWidget): self.manual_button = QPushButton(Translations["entries.unlinked.relink.manual"]) self.manual_button.setHidden(True) - self.delete_button = QPushButton(Translations["entries.unlinked.delete_alt"]) - self.delete_modal = DeleteUnlinkedEntriesModal(self.driver, self.tracker) - self.delete_modal.done.connect( + self.remove_button = QPushButton(Translations["entries.unlinked.remove_alt"]) + self.remove_modal = RemoveUnlinkedEntriesModal(self.driver, self.tracker) + self.remove_modal.done.connect( lambda: ( - self.set_missing_count(), + self.set_unlinked_count(), # refresh the grid self.driver.update_browsing_state(), + self.refresh_unlinked(), ) ) - self.delete_button.clicked.connect(self.delete_modal.show) + self.remove_button.clicked.connect(self.remove_modal.show) self.button_container = QWidget() self.button_layout = QHBoxLayout(self.button_container) @@ -93,19 +93,19 @@ class FixUnlinkedEntriesModal(QWidget): self.done_button.clicked.connect(self.hide) self.button_layout.addWidget(self.done_button) - self.root_layout.addWidget(self.missing_count_label) + self.root_layout.addWidget(self.unlinked_count_label) self.root_layout.addWidget(self.unlinked_desc_widget) self.root_layout.addWidget(self.refresh_unlinked_button) self.root_layout.addWidget(self.search_button) self.root_layout.addWidget(self.manual_button) - self.root_layout.addWidget(self.delete_button) + self.root_layout.addWidget(self.remove_button) self.root_layout.addStretch(1) self.root_layout.addStretch(2) self.root_layout.addWidget(self.button_container) - self.set_missing_count(self.missing_count) + self.update_unlinked_count() - def refresh_missing_files(self): + def refresh_unlinked(self): pw = ProgressWidget( cancel_button_text=None, minimum=0, @@ -114,30 +114,47 @@ class FixUnlinkedEntriesModal(QWidget): pw.setWindowTitle(Translations["library.scan_library.title"]) pw.update_label(Translations["entries.unlinked.scanning"]) + def update_driver_widgets(): + if ( + hasattr(self.driver, "library_info_window") + and self.driver.library_info_window.isVisible() + ): + self.driver.library_info_window.update_cleanup() + pw.from_iterable_function( - self.tracker.refresh_missing_files, + self.tracker.refresh_unlinked_files, None, - self.set_missing_count, - self.delete_modal.refresh_list, + self.set_unlinked_count, + self.update_unlinked_count, + self.remove_modal.refresh_list, + update_driver_widgets, ) - def set_missing_count(self, count: int | None = None): - if count is not None: - self.missing_count = count - else: - self.missing_count = self.tracker.missing_file_entries_count + def set_unlinked_count(self): + """Sets the unlinked_entries_count in the Library to the tracker's value.""" + self.lib.unlinked_entries_count = self.tracker.unlinked_entries_count - if self.missing_count < 0: - self.search_button.setDisabled(True) - self.delete_button.setDisabled(True) - self.missing_count_label.setText(Translations["entries.unlinked.missing_count.none"]) - else: - # disable buttons if there are no files to fix - self.search_button.setDisabled(self.missing_count == 0) - self.delete_button.setDisabled(self.missing_count == 0) - self.missing_count_label.setText( - Translations.format("entries.unlinked.missing_count.some", count=self.missing_count) - ) + def update_unlinked_count(self): + """Updates the UI to reflect the Library's current unlinked_entries_count.""" + # Indicates that the library is new compared to the last update. + # NOTE: Make sure set_unlinked_count() is called before this! + if self.tracker.unlinked_entries_count > 0 and self.lib.unlinked_entries_count < 0: + self.tracker.reset() + + count: int = self.lib.unlinked_entries_count + + self.search_button.setDisabled(count < 1) + self.remove_button.setDisabled(count < 1) + + count_text: str = Translations.format( + "entries.unlinked.unlinked_count", count=count if count >= 0 else "—" + ) + self.unlinked_count_label.setText(f"

{count_text}

") + + @override + def showEvent(self, event: QtGui.QShowEvent) -> None: + self.update_unlinked_count() + return super().showEvent(event) @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 diff --git a/src/tagstudio/qt/modals/mirror_entities.py b/src/tagstudio/qt/modals/mirror_entries_modal.py similarity index 100% rename from src/tagstudio/qt/modals/mirror_entities.py rename to src/tagstudio/qt/modals/mirror_entries_modal.py diff --git a/src/tagstudio/qt/modals/relink_unlinked.py b/src/tagstudio/qt/modals/relink_entries_modal.py similarity index 78% rename from src/tagstudio/qt/modals/relink_unlinked.py rename to src/tagstudio/qt/modals/relink_entries_modal.py index 8bd3ea81..81603a25 100644 --- a/src/tagstudio/qt/modals/relink_unlinked.py +++ b/src/tagstudio/qt/modals/relink_entries_modal.py @@ -5,7 +5,7 @@ from PySide6.QtCore import QObject, Signal -from tagstudio.core.utils.missing_files import MissingRegistry +from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.progress import ProgressWidget @@ -13,7 +13,7 @@ from tagstudio.qt.widgets.progress import ProgressWidget class RelinkUnlinkedEntries(QObject): done = Signal() - def __init__(self, tracker: MissingRegistry): + def __init__(self, tracker: UnlinkedRegistry): super().__init__() self.tracker = tracker @@ -21,8 +21,8 @@ class RelinkUnlinkedEntries(QObject): def displayed_text(x): return Translations.format( "entries.unlinked.relink.attempting", - idx=x, - missing_count=self.tracker.missing_file_entries_count, + index=x, + unlinked_count=self.tracker.unlinked_entries_count, fixed_count=self.tracker.files_fixed_count, ) @@ -30,8 +30,7 @@ class RelinkUnlinkedEntries(QObject): label_text="", cancel_button_text=None, minimum=0, - maximum=self.tracker.missing_file_entries_count, + maximum=self.tracker.unlinked_entries_count, ) pw.setWindowTitle(Translations["entries.unlinked.relink.title"]) - pw.from_iterable_function(self.tracker.fix_unlinked_entries, displayed_text, self.done.emit) diff --git a/src/tagstudio/qt/modals/delete_unlinked.py b/src/tagstudio/qt/modals/remove_ignored_modal.py similarity index 79% rename from src/tagstudio/qt/modals/delete_unlinked.py rename to src/tagstudio/qt/modals/remove_ignored_modal.py index f3902cce..b9e02789 100644 --- a/src/tagstudio/qt/modals/delete_unlinked.py +++ b/src/tagstudio/qt/modals/remove_ignored_modal.py @@ -17,7 +17,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from tagstudio.core.utils.missing_files import MissingRegistry +from tagstudio.core.utils.ignored_registry import IgnoredRegistry from tagstudio.qt.helpers.custom_runnable import CustomRunnable from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.progress import ProgressWidget @@ -27,14 +27,14 @@ if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver -class DeleteUnlinkedEntriesModal(QWidget): +class RemoveIgnoredModal(QWidget): done = Signal() - def __init__(self, driver: "QtDriver", tracker: MissingRegistry): + def __init__(self, driver: "QtDriver", tracker: IgnoredRegistry): super().__init__() self.driver = driver self.tracker = tracker - self.setWindowTitle(Translations["entries.unlinked.delete"]) + self.setWindowTitle(Translations["entries.ignored.remove"]) self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setMinimumSize(500, 400) self.root_layout = QVBoxLayout(self) @@ -42,8 +42,8 @@ class DeleteUnlinkedEntriesModal(QWidget): self.desc_widget = QLabel( Translations.format( - "entries.unlinked.delete.confirm", - count=self.tracker.missing_file_entries_count, + "entries.remove.plural.confirm", + count=self.tracker.ignored_count, ) ) self.desc_widget.setObjectName("descriptionLabel") @@ -64,7 +64,7 @@ class DeleteUnlinkedEntriesModal(QWidget): self.cancel_button.clicked.connect(self.hide) self.button_layout.addWidget(self.cancel_button) - self.delete_button = QPushButton(Translations["generic.delete_alt"]) + self.delete_button = QPushButton(Translations["generic.remove_alt"]) self.delete_button.clicked.connect(self.hide) self.delete_button.clicked.connect(lambda: self.delete_entries()) self.button_layout.addWidget(self.delete_button) @@ -75,13 +75,11 @@ class DeleteUnlinkedEntriesModal(QWidget): def refresh_list(self): self.desc_widget.setText( - Translations.format( - "entries.unlinked.delete.confirm", count=self.tracker.missing_file_entries_count - ) + Translations.format("entries.remove.plural.confirm", count=self.tracker.ignored_count) ) self.model.clear() - for i in self.tracker.missing_file_entries: + for i in self.tracker.ignored_entries: item = QStandardItem(str(i.path)) item.setEditable(False) self.model.appendRow(item) @@ -92,11 +90,15 @@ class DeleteUnlinkedEntriesModal(QWidget): minimum=0, maximum=0, ) - pw.setWindowTitle(Translations["entries.unlinked.delete.deleting"]) - pw.update_label(Translations["entries.unlinked.delete.deleting"]) + pw.setWindowTitle(Translations["entries.generic.remove.removing"]) + pw.update_label( + Translations.format( + "entries.generic.remove.removing_count", count=self.tracker.ignored_count + ) + ) pw.show() - r = CustomRunnable(self.tracker.execute_deletion) + r = CustomRunnable(self.tracker.remove_ignored_entries) QThreadPool.globalInstance().start(r) r.done.connect( lambda: ( diff --git a/src/tagstudio/qt/modals/remove_unlinked_modal.py b/src/tagstudio/qt/modals/remove_unlinked_modal.py new file mode 100644 index 00000000..cfd3fca7 --- /dev/null +++ b/src/tagstudio/qt/modals/remove_unlinked_modal.py @@ -0,0 +1,119 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import TYPE_CHECKING, override + +from PySide6 import QtCore, QtGui +from PySide6.QtCore import Qt, QThreadPool, Signal +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QListView, + QPushButton, + QVBoxLayout, + QWidget, +) + +from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry +from tagstudio.qt.helpers.custom_runnable import CustomRunnable +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget + +# Only import for type checking/autocompletion, will not be imported at runtime. +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +class RemoveUnlinkedEntriesModal(QWidget): + done = Signal() + + def __init__(self, driver: "QtDriver", tracker: UnlinkedRegistry): + super().__init__() + self.driver = driver + self.tracker = tracker + self.setWindowTitle(Translations["entries.unlinked.remove"]) + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setMinimumSize(500, 400) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 6, 6, 6) + + self.desc_widget = QLabel( + Translations.format( + "entries.remove.plural.confirm", + count=self.tracker.unlinked_entries_count, + ) + ) + self.desc_widget.setObjectName("descriptionLabel") + self.desc_widget.setWordWrap(True) + self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.list_view = QListView() + self.model = QStandardItemModel() + self.list_view.setModel(self.model) + + self.button_container = QWidget() + self.button_layout = QHBoxLayout(self.button_container) + self.button_layout.setContentsMargins(6, 6, 6, 6) + self.button_layout.addStretch(1) + + self.cancel_button = QPushButton(Translations["generic.cancel_alt"]) + self.cancel_button.setDefault(True) + self.cancel_button.clicked.connect(self.hide) + self.button_layout.addWidget(self.cancel_button) + + self.delete_button = QPushButton(Translations["generic.remove_alt"]) + self.delete_button.clicked.connect(self.hide) + self.delete_button.clicked.connect(lambda: self.remove_entries()) + self.button_layout.addWidget(self.delete_button) + + self.root_layout.addWidget(self.desc_widget) + self.root_layout.addWidget(self.list_view) + self.root_layout.addWidget(self.button_container) + + def refresh_list(self): + self.desc_widget.setText( + Translations.format( + "entries.remove.plural.confirm", count=self.tracker.unlinked_entries_count + ) + ) + + self.model.clear() + for i in self.tracker.unlinked_entries: + item = QStandardItem(str(i.path)) + item.setEditable(False) + self.model.appendRow(item) + + def remove_entries(self): + pw = ProgressWidget( + cancel_button_text=None, + minimum=0, + maximum=0, + ) + pw.setWindowTitle(Translations["entries.generic.remove.removing"]) + pw.update_label( + Translations.format( + "entries.generic.remove.removing_count", count=self.tracker.unlinked_entries_count + ) + ) + pw.show() + + r = CustomRunnable(self.tracker.remove_unlinked_entries) + QThreadPool.globalInstance().start(r) + r.done.connect( + lambda: ( + pw.hide(), + pw.deleteLater(), + self.done.emit(), + ) + ) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.cancel_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 023b4d19..34b46ef7 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -1,138 +1,146 @@ { - "splash_classic": { - "path": "qt/images/splash/classic.png", - "mode": "qpixmap" - }, - "splash_goo_gears": { - "path": "qt/images/splash/goo_gears.png", - "mode": "qpixmap" - }, - "icon": { - "path": "icon.png", - "mode": "pil" - }, - "play_icon": { - "path": "qt/images/play.svg", - "mode": "rb" - }, - "pause_icon": { - "path": "qt/images/pause.svg", - "mode": "rb" - }, - "volume_icon": { - "path": "qt/images/volume.svg", - "mode": "rb" - }, - "volume_mute_icon": { - "path": "qt/images/volume_mute.svg", - "mode": "rb" - }, - "broken_link_icon": { - "path": "qt/images/broken_link_icon.png", - "mode": "pil" - }, - "ignored": { - "path": "qt/images/ignored_128.png", - "mode": "pil" - }, - "adobe_illustrator": { - "path": "qt/images/file_icons/adobe_illustrator.png", - "mode": "pil" - }, - "adobe_photoshop": { - "path": "qt/images/file_icons/adobe_photoshop.png", - "mode": "pil" - }, - "affinity_photo": { - "path": "qt/images/file_icons/affinity_photo.png", - "mode": "pil" - }, - "archive": { - "path": "qt/images/file_icons/archive.png", - "mode": "pil" - }, - "audio": { - "path": "qt/images/file_icons/audio.png", - "mode": "pil" - }, - "blender": { - "path": "qt/images/file_icons/blender.png", - "mode": "pil" - }, - "database": { - "path": "qt/images/file_icons/database.png", - "mode": "pil" - }, - "document": { - "path": "qt/images/file_icons/document.png", - "mode": "pil" - }, - "ebook": { - "path": "qt/images/file_icons/ebook.png", - "mode": "pil" - }, - "file_generic": { - "path": "qt/images/file_icons/file_generic.png", - "mode": "pil" - }, - "font": { - "path": "qt/images/file_icons/font.png", - "mode": "pil" - }, - "image": { - "path": "qt/images/file_icons/image.png", - "mode": "pil" - }, - "image_vector": { - "path": "qt/images/file_icons/image_vector.png", - "mode": "pil" - }, - "material": { - "path": "qt/images/file_icons/material.png", - "mode": "pil" - }, - "model": { - "path": "qt/images/file_icons/model.png", - "mode": "pil" - }, - "presentation": { - "path": "qt/images/file_icons/presentation.png", - "mode": "pil" - }, - "program": { - "path": "qt/images/file_icons/program.png", - "mode": "pil" - }, - "shader": { - "path": "qt/images/file_icons/shader.png", - "mode": "pil" - }, - "shortcut": { - "path": "qt/images/file_icons/shortcut.png", - "mode": "pil" - }, - "spreadsheet": { - "path": "qt/images/file_icons/spreadsheet.png", - "mode": "pil" - }, - "text": { - "path": "qt/images/file_icons/text.png", - "mode": "pil" - }, - "video": { - "path": "qt/images/file_icons/video.png", - "mode": "pil" - }, - "thumb_loading": { - "path": "qt/images/thumb_loading.png", - "mode": "pil" - }, - "bxs-left-arrow": { - "path": "qt/images/bxs-left-arrow.png", - "mode": "pil" - }, - "bxs-right-arrow": { - "path": "qt/images/bxs-right-arrow.png", - "mode": "pil" - } + "splash_classic": { + "path": "qt/images/splash/classic.png", + "mode": "qpixmap" + }, + "splash_goo_gears": { + "path": "qt/images/splash/goo_gears.png", + "mode": "qpixmap" + }, + "icon": { + "path": "icon.png", + "mode": "pil" + }, + "play_icon": { + "path": "qt/images/play.svg", + "mode": "rb" + }, + "pause_icon": { + "path": "qt/images/pause.svg", + "mode": "rb" + }, + "volume_icon": { + "path": "qt/images/volume.svg", + "mode": "rb" + }, + "volume_mute_icon": { + "path": "qt/images/volume_mute.svg", + "mode": "rb" + }, + "broken_link_icon": { + "path": "qt/images/broken_link_icon.png", + "mode": "pil" + }, + "ignored": { + "path": "qt/images/ignored_128.png", + "mode": "pil" + }, + "adobe_illustrator": { + "path": "qt/images/file_icons/adobe_illustrator.png", + "mode": "pil" + }, + "adobe_photoshop": { + "path": "qt/images/file_icons/adobe_photoshop.png", + "mode": "pil" + }, + "affinity_photo": { + "path": "qt/images/file_icons/affinity_photo.png", + "mode": "pil" + }, + "archive": { + "path": "qt/images/file_icons/archive.png", + "mode": "pil" + }, + "audio": { + "path": "qt/images/file_icons/audio.png", + "mode": "pil" + }, + "database": { + "path": "qt/images/file_icons/database.png", + "mode": "pil" + }, + "document": { + "path": "qt/images/file_icons/document.png", + "mode": "pil" + }, + "ebook": { + "path": "qt/images/file_icons/ebook.png", + "mode": "pil" + }, + "file_generic": { + "path": "qt/images/file_icons/file_generic.png", + "mode": "pil" + }, + "font": { + "path": "qt/images/file_icons/font.png", + "mode": "pil" + }, + "image": { + "path": "qt/images/file_icons/image.png", + "mode": "pil" + }, + "image_vector": { + "path": "qt/images/file_icons/image_vector.png", + "mode": "pil" + }, + "material": { + "path": "qt/images/file_icons/material.png", + "mode": "pil" + }, + "model": { + "path": "qt/images/file_icons/model.png", + "mode": "pil" + }, + "presentation": { + "path": "qt/images/file_icons/presentation.png", + "mode": "pil" + }, + "program": { + "path": "qt/images/file_icons/program.png", + "mode": "pil" + }, + "shader": { + "path": "qt/images/file_icons/shader.png", + "mode": "pil" + }, + "shortcut": { + "path": "qt/images/file_icons/shortcut.png", + "mode": "pil" + }, + "spreadsheet": { + "path": "qt/images/file_icons/spreadsheet.png", + "mode": "pil" + }, + "text": { + "path": "qt/images/file_icons/text.png", + "mode": "pil" + }, + "video": { + "path": "qt/images/file_icons/video.png", + "mode": "pil" + }, + "thumb_loading": { + "path": "qt/images/thumb_loading.png", + "mode": "pil" + }, + "bxs-left-arrow": { + "path": "qt/images/bxs-left-arrow.png", + "mode": "pil" + }, + "bxs-right-arrow": { + "path": "qt/images/bxs-right-arrow.png", + "mode": "pil" + }, + "unlinked_stat": { + "path": "qt/images/unlinked_stat.png", + "mode": "pil" + }, + "ignored_stat": { + "path": "qt/images/ignored_stat.png", + "mode": "pil" + }, + "dupe_file_stat": { + "path": "qt/images/dupe_file_stat.png", + "mode": "pil" + } } diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 39abd18b..7830bcef 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -45,7 +45,6 @@ from PySide6.QtWidgets import ( QScrollArea, ) -# this import has side-effect of import PySide resources import tagstudio.qt.resources_rc # noqa: F401 from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH from tagstudio.core.driver import DriverMixin @@ -69,6 +68,9 @@ from tagstudio.core.utils.refresh_dir import RefreshDirTracker from tagstudio.core.utils.types import unwrap from tagstudio.core.utils.web import strip_web_protocol from tagstudio.qt.cache_manager import CacheManager + +# this import has side-effect of import PySide resources +from tagstudio.qt.controller.fix_ignored_modal_controller import FixIgnoredEntriesModal from tagstudio.qt.controller.widgets.ignore_modal_controller import IgnoreModal from tagstudio.qt.controller.widgets.library_info_window_controller import LibraryInfoWindow from tagstudio.qt.helpers.custom_runnable import CustomRunnable @@ -180,6 +182,7 @@ class QtDriver(DriverMixin, QObject): folders_modal: FoldersToTagsModal about_modal: AboutModal unlinked_modal: FixUnlinkedEntriesModal + ignored_modal: FixIgnoredEntriesModal dupe_modal: FixDupeFilesModal library_info_window: LibraryInfoWindow @@ -503,6 +506,15 @@ class QtDriver(DriverMixin, QObject): create_fix_unlinked_entries_modal ) + def create_ignored_entries_modal(): + if not hasattr(self, "ignored_modal"): + self.ignored_modal = FixIgnoredEntriesModal(self.lib, self) + self.ignored_modal.show() + + self.main_window.menu_bar.fix_ignored_entries_action.triggered.connect( + create_ignored_entries_modal + ) + def create_dupe_files_modal(): if not hasattr(self, "dupe_modal"): self.dupe_modal = FixDupeFilesModal(self.lib, self) @@ -753,6 +765,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.menu_bar.ignore_modal_action.setEnabled(False) self.main_window.menu_bar.new_tag_action.setEnabled(False) self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(False) + self.main_window.menu_bar.fix_ignored_entries_action.setEnabled(False) self.main_window.menu_bar.fix_dupe_files_action.setEnabled(False) self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(False) self.main_window.menu_bar.folders_to_tags_action.setEnabled(False) @@ -1747,6 +1760,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.menu_bar.ignore_modal_action.setEnabled(True) self.main_window.menu_bar.new_tag_action.setEnabled(True) self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(True) + self.main_window.menu_bar.fix_ignored_entries_action.setEnabled(True) self.main_window.menu_bar.fix_dupe_files_action.setEnabled(True) self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(True) self.main_window.menu_bar.folders_to_tags_action.setEnabled(True) diff --git a/src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py b/src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py new file mode 100644 index 00000000..b198a2b4 --- /dev/null +++ b/src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py @@ -0,0 +1,67 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import TYPE_CHECKING, override + +from PySide6 import QtCore, QtGui +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.translations import Translations + +# Only import for type checking/autocompletion, will not be imported at runtime. +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +class FixIgnoredEntriesModalView(QWidget): + def __init__(self, library: "Library", driver: "QtDriver"): + super().__init__() + self.lib = library + self.driver = driver + + self.setWindowTitle(Translations["entries.ignored.title"]) + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setMinimumSize(400, 300) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 6, 6, 6) + + self.ignored_desc_widget = QLabel(Translations["entries.ignored.description"]) + self.ignored_desc_widget.setObjectName("ignoredDescriptionLabel") + self.ignored_desc_widget.setWordWrap(True) + self.ignored_desc_widget.setStyleSheet("text-align:left;") + + self.ignored_count_label = QLabel() + self.ignored_count_label.setObjectName("ignoredCountLabel") + self.ignored_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.refresh_ignored_button = QPushButton(Translations["entries.generic.refresh_alt"]) + self.remove_button = QPushButton(Translations["entries.ignored.remove_alt"]) + + self.button_container = QWidget() + self.button_layout = QHBoxLayout(self.button_container) + self.button_layout.setContentsMargins(6, 6, 6, 6) + self.button_layout.addStretch(1) + + self.done_button = QPushButton(Translations["generic.done_alt"]) + self.done_button.setDefault(True) + self.button_layout.addWidget(self.done_button) + + self.root_layout.addWidget(self.ignored_count_label) + self.root_layout.addWidget(self.ignored_desc_widget) + self.root_layout.addWidget(self.refresh_ignored_button) + self.root_layout.addWidget(self.remove_button) + self.root_layout.addStretch(1) + self.root_layout.addStretch(2) + self.root_layout.addWidget(self.button_container) + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 + if event.key() == QtCore.Qt.Key.Key_Escape: + self.done_button.click() + else: # Other key presses + pass + return super().keyPressEvent(event) diff --git a/src/tagstudio/qt/view/widgets/library_info_window_view.py b/src/tagstudio/qt/view/widgets/library_info_window_view.py index 0a6afc88..694d9259 100644 --- a/src/tagstudio/qt/view/widgets/library_info_window_view.py +++ b/src/tagstudio/qt/view/widgets/library_info_window_view.py @@ -3,10 +3,15 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import math from typing import TYPE_CHECKING +from PIL import Image, ImageQt from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( + QFrame, + QGraphicsOpacityEffect, QGridLayout, QHBoxLayout, QLabel, @@ -17,11 +22,13 @@ from PySide6.QtWidgets import ( QWidget, ) -from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.platform_strings import open_file_str from tagstudio.qt.translations import Translations # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: + from tagstudio.core.library.alchemy.library import Library from tagstudio.qt.ts_qt import QtDriver @@ -32,11 +39,12 @@ class LibraryInfoWindowView(QWidget): self.driver = driver self.setWindowTitle("Library Information") - self.setMinimumSize(400, 300) + self.setMinimumSize(800, 480) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(6, 6, 6, 6) row_height: int = 22 + icon_margin: int = 4 cell_alignment: Qt.AlignmentFlag = ( Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter ) @@ -49,11 +57,12 @@ class LibraryInfoWindowView(QWidget): self.body_widget = QWidget() self.body_layout = QHBoxLayout(self.body_widget) self.body_layout.setContentsMargins(0, 0, 0, 0) - self.body_layout.setSpacing(0) + self.body_layout.setSpacing(6) # Statistics ----------------------------------------------------------- self.stats_widget = QWidget() self.stats_layout = QVBoxLayout(self.stats_widget) + self.stats_layout.setAlignment(Qt.AlignmentFlag.AlignTop) self.stats_layout.setContentsMargins(0, 0, 0, 0) self.stats_layout.setSpacing(12) @@ -67,16 +76,17 @@ class LibraryInfoWindowView(QWidget): self.stats_grid_layout.setColumnMinimumWidth(1, 12) self.stats_grid_layout.setColumnMinimumWidth(3, 12) - self.entries_row: int = 0 - self.tags_row: int = 1 - self.fields_row: int = 2 - self.namespaces_row: int = 3 - self.colors_row: int = 4 - self.macros_row: int = 5 + self.stats_entries_row: int = 0 + self.stats_tags_row: int = 1 + self.stats_fields_row: int = 2 + self.stats_namespaces_row: int = 3 + self.stats_colors_row: int = 4 + self.stats_macros_row: int = 5 - self.labels_col: int = 0 - self.values_col: int = 2 - self.buttons_col: int = 4 + # NOTE: Alternating rows for visual padding + self.stats_labels_col: int = 0 + self.stats_values_col: int = 2 + self.stats_buttons_col: int = 4 self.entries_label: QLabel = QLabel(Translations["library_info.stats.entries"]) self.entries_label.setAlignment(cell_alignment) @@ -91,21 +101,43 @@ class LibraryInfoWindowView(QWidget): self.macros_label: QLabel = QLabel(Translations["library_info.stats.macros"]) self.macros_label.setAlignment(cell_alignment) - self.stats_grid_layout.addWidget(self.entries_label, self.entries_row, self.labels_col) - self.stats_grid_layout.addWidget(self.tags_label, self.tags_row, self.labels_col) - self.stats_grid_layout.addWidget(self.fields_label, self.fields_row, self.labels_col) self.stats_grid_layout.addWidget( - self.namespaces_label, self.namespaces_row, self.labels_col + self.entries_label, + self.stats_entries_row, + self.stats_labels_col, + ) + self.stats_grid_layout.addWidget( + self.tags_label, + self.stats_tags_row, + self.stats_labels_col, + ) + self.stats_grid_layout.addWidget( + self.fields_label, + self.stats_fields_row, + self.stats_labels_col, + ) + self.stats_grid_layout.addWidget( + self.namespaces_label, + self.stats_namespaces_row, + self.stats_labels_col, + ) + self.stats_grid_layout.addWidget( + self.colors_label, + self.stats_colors_row, + self.stats_labels_col, + ) + self.stats_grid_layout.addWidget( + self.macros_label, + self.stats_macros_row, + self.stats_labels_col, ) - self.stats_grid_layout.addWidget(self.colors_label, self.colors_row, self.labels_col) - self.stats_grid_layout.addWidget(self.macros_label, self.macros_row, self.labels_col) - self.stats_grid_layout.setRowMinimumHeight(self.entries_row, row_height) - self.stats_grid_layout.setRowMinimumHeight(self.tags_row, row_height) - self.stats_grid_layout.setRowMinimumHeight(self.fields_row, row_height) - self.stats_grid_layout.setRowMinimumHeight(self.namespaces_row, row_height) - self.stats_grid_layout.setRowMinimumHeight(self.colors_row, row_height) - self.stats_grid_layout.setRowMinimumHeight(self.macros_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_entries_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_tags_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_fields_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_namespaces_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_colors_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.stats_macros_row, row_height) self.entry_count_label: QLabel = QLabel() self.entry_count_label.setAlignment(cell_alignment) @@ -120,21 +152,49 @@ class LibraryInfoWindowView(QWidget): self.macros_count_label: QLabel = QLabel() self.macros_count_label.setAlignment(cell_alignment) - self.stats_grid_layout.addWidget(self.entry_count_label, self.entries_row, self.values_col) - self.stats_grid_layout.addWidget(self.tag_count_label, self.tags_row, self.values_col) - self.stats_grid_layout.addWidget(self.field_count_label, self.fields_row, self.values_col) self.stats_grid_layout.addWidget( - self.namespaces_count_label, self.namespaces_row, self.values_col + self.entry_count_label, + self.stats_entries_row, + self.stats_values_col, + ) + self.stats_grid_layout.addWidget( + self.tag_count_label, + self.stats_tags_row, + self.stats_values_col, + ) + self.stats_grid_layout.addWidget( + self.field_count_label, + self.stats_fields_row, + self.stats_values_col, + ) + self.stats_grid_layout.addWidget( + self.namespaces_count_label, + self.stats_namespaces_row, + self.stats_values_col, + ) + self.stats_grid_layout.addWidget( + self.color_count_label, + self.stats_colors_row, + self.stats_values_col, + ) + self.stats_grid_layout.addWidget( + self.macros_count_label, + self.stats_macros_row, + self.stats_values_col, ) - self.stats_grid_layout.addWidget(self.color_count_label, self.colors_row, self.values_col) - self.stats_grid_layout.addWidget(self.macros_count_label, self.macros_row, self.values_col) self.manage_tags_button = QPushButton(Translations["edit.tag_manager"]) self.manage_colors_button = QPushButton(Translations["color_manager.title"]) - self.stats_grid_layout.addWidget(self.manage_tags_button, self.tags_row, self.buttons_col) self.stats_grid_layout.addWidget( - self.manage_colors_button, self.colors_row, self.buttons_col + self.manage_tags_button, + self.stats_tags_row, + self.stats_buttons_col, + ) + self.stats_grid_layout.addWidget( + self.manage_colors_button, + self.stats_colors_row, + self.stats_buttons_col, ) self.stats_layout.addWidget(self.stats_label) @@ -148,7 +208,246 @@ class LibraryInfoWindowView(QWidget): QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) ) - # Buttons + # Vertical Separator + self.vertical_sep = QFrame() + self.vertical_sep.setFrameShape(QFrame.Shape.VLine) + self.vertical_sep.setFrameShadow(QFrame.Shadow.Plain) + opacity_effect_vert_sep = QGraphicsOpacityEffect(self) + opacity_effect_vert_sep.setOpacity(0.1) + self.vertical_sep.setGraphicsEffect(opacity_effect_vert_sep) + self.body_layout.addWidget(self.vertical_sep) + + # Cleanup -------------------------------------------------------------- + self.cleanup_widget = QWidget() + self.cleanup_layout = QVBoxLayout(self.cleanup_widget) + self.cleanup_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.cleanup_layout.setContentsMargins(0, 0, 0, 0) + self.cleanup_layout.setSpacing(12) + + self.cleanup_label = QLabel(f"

{Translations['library_info.cleanup']}

") + self.cleanup_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.cleanup_grid: QWidget = QWidget() + self.cleanup_grid_layout: QGridLayout = QGridLayout(self.cleanup_grid) + self.cleanup_grid_layout.setContentsMargins(0, 0, 0, 0) + self.cleanup_grid_layout.setSpacing(0) + self.cleanup_grid_layout.setColumnMinimumWidth(1, 12) + self.cleanup_grid_layout.setColumnMinimumWidth(3, 6) + self.cleanup_grid_layout.setColumnMinimumWidth(5, 6) + + self.cleanup_layout.addWidget(self.cleanup_label) + self.cleanup_layout.addWidget(self.cleanup_grid) + + self.cleanup_unlinked_row: int = 0 + self.cleanup_ignored_row: int = 1 + self.cleanup_dupe_files_row: int = 2 + self.cleanup_section_break_row: int = 3 + self.cleanup_legacy_json_row: int = 4 + self.cleanup_backups_row: int = 5 + + # NOTE: Alternating rows for visual padding + self.cleanup_labels_col: int = 0 + self.cleanup_values_col: int = 2 + self.cleanup_icons_col: int = 4 + self.cleanup_buttons_col: int = 6 + + # Horizontal Separator + self.horizontal_sep = QFrame() + self.horizontal_sep.setFrameShape(QFrame.Shape.HLine) + self.horizontal_sep.setFrameShadow(QFrame.Shadow.Plain) + self.horizontal_sep.setFixedHeight(row_height) + opacity_effect_hor_sep = QGraphicsOpacityEffect(self) + opacity_effect_hor_sep.setOpacity(0.1) + self.horizontal_sep.setGraphicsEffect(opacity_effect_hor_sep) + self.cleanup_grid_layout.addWidget( + self.horizontal_sep, + self.cleanup_section_break_row, + self.cleanup_labels_col, + 1, + 7, + Qt.AlignmentFlag.AlignVCenter, + ) + + self.unlinked_icon = QLabel() + unlinked_image: Image.Image = self.driver.rm.get("unlinked_stat") # pyright: ignore[reportAssignmentType] + unlinked_pixmap = QPixmap.fromImage(ImageQt.ImageQt(unlinked_image)) + unlinked_pixmap.setDevicePixelRatio(self.devicePixelRatio()) + unlinked_pixmap = unlinked_pixmap.scaledToWidth( + math.floor((row_height - icon_margin) * self.devicePixelRatio()), + Qt.TransformationMode.SmoothTransformation, + ) + self.unlinked_icon.setPixmap(unlinked_pixmap) + + self.ignored_icon = QLabel() + ignored_image: Image.Image = self.driver.rm.get("ignored_stat") # pyright: ignore[reportAssignmentType] + ignored_pixmap = QPixmap.fromImage(ImageQt.ImageQt(ignored_image)) + ignored_pixmap.setDevicePixelRatio(self.devicePixelRatio()) + ignored_pixmap = ignored_pixmap.scaledToWidth( + math.floor((row_height - icon_margin) * self.devicePixelRatio()), + Qt.TransformationMode.SmoothTransformation, + ) + self.ignored_icon.setPixmap(ignored_pixmap) + + self.dupe_file_icon = QLabel() + dupe_file_image: Image.Image = self.driver.rm.get("dupe_file_stat") # pyright: ignore[reportAssignmentType] + dupe_file_pixmap = QPixmap.fromImage( + ImageQt.ImageQt(theme_fg_overlay(dupe_file_image, use_alpha=False)) + ) + dupe_file_pixmap.setDevicePixelRatio(self.devicePixelRatio()) + dupe_file_pixmap = dupe_file_pixmap.scaledToWidth( + math.floor((row_height - icon_margin) * self.devicePixelRatio()), + Qt.TransformationMode.SmoothTransformation, + ) + self.dupe_file_icon.setPixmap(dupe_file_pixmap) + + self.cleanup_grid_layout.addWidget( + self.unlinked_icon, + self.cleanup_unlinked_row, + self.cleanup_icons_col, + ) + self.cleanup_grid_layout.addWidget( + self.ignored_icon, + self.cleanup_ignored_row, + self.cleanup_icons_col, + ) + self.cleanup_grid_layout.addWidget( + self.dupe_file_icon, + self.cleanup_dupe_files_row, + self.cleanup_icons_col, + ) + + self.unlinked_label: QLabel = QLabel(Translations["library_info.cleanup.unlinked"]) + self.unlinked_label.setAlignment(cell_alignment) + self.ignored_label: QLabel = QLabel(Translations["library_info.cleanup.ignored"]) + self.ignored_label.setAlignment(cell_alignment) + self.dupe_files_label: QLabel = QLabel(Translations["library_info.cleanup.dupe_files"]) + self.dupe_files_label.setAlignment(cell_alignment) + self.legacy_json_label: QLabel = QLabel(Translations["library_info.cleanup.legacy_json"]) + self.legacy_json_label.setAlignment(cell_alignment) + self.backups_label: QLabel = QLabel(Translations["library_info.cleanup.backups"]) + self.backups_label.setAlignment(cell_alignment) + + self.cleanup_grid_layout.addWidget( + self.unlinked_label, + self.cleanup_unlinked_row, + self.cleanup_labels_col, + ) + self.cleanup_grid_layout.addWidget( + self.ignored_label, + self.cleanup_ignored_row, + self.cleanup_labels_col, + ) + self.cleanup_grid_layout.addWidget( + self.dupe_files_label, + self.cleanup_dupe_files_row, + self.cleanup_labels_col, + ) + self.cleanup_grid_layout.addWidget( + self.legacy_json_label, + self.cleanup_legacy_json_row, + self.cleanup_labels_col, + ) + self.cleanup_grid_layout.addWidget( + self.backups_label, + self.cleanup_backups_row, + self.cleanup_labels_col, + ) + + self.cleanup_grid_layout.setRowMinimumHeight(self.cleanup_unlinked_row, row_height) + self.cleanup_grid_layout.setRowMinimumHeight(self.cleanup_ignored_row, row_height) + self.cleanup_grid_layout.setRowMinimumHeight(self.cleanup_dupe_files_row, row_height) + self.cleanup_grid_layout.setRowMinimumHeight(self.cleanup_legacy_json_row, row_height) + self.cleanup_grid_layout.setRowMinimumHeight(self.cleanup_backups_row, row_height) + + self.unlinked_count_label: QLabel = QLabel() + self.unlinked_count_label.setAlignment(cell_alignment) + self.ignored_count_label: QLabel = QLabel() + self.ignored_count_label.setAlignment(cell_alignment) + self.dupe_files_count_label: QLabel = QLabel() + self.dupe_files_count_label.setAlignment(cell_alignment) + self.legacy_json_status_label: QLabel = QLabel() + self.legacy_json_status_label.setAlignment(cell_alignment) + self.backups_count_label: QLabel = QLabel() + self.backups_count_label.setAlignment(cell_alignment) + + self.cleanup_grid_layout.addWidget( + self.unlinked_count_label, + self.cleanup_unlinked_row, + self.cleanup_values_col, + ) + self.cleanup_grid_layout.addWidget( + self.ignored_count_label, + self.cleanup_ignored_row, + self.cleanup_values_col, + ) + self.cleanup_grid_layout.addWidget( + self.dupe_files_count_label, + self.cleanup_dupe_files_row, + self.cleanup_values_col, + ) + self.cleanup_grid_layout.addWidget( + self.legacy_json_status_label, + self.cleanup_legacy_json_row, + self.cleanup_values_col, + ) + self.cleanup_grid_layout.addWidget( + self.backups_count_label, + self.cleanup_backups_row, + self.cleanup_values_col, + ) + + self.fix_unlinked_entries = QPushButton(Translations["menu.tools.fix_unlinked_entries"]) + self.fix_ignored_entries = QPushButton(Translations["menu.tools.fix_ignored_entries"]) + self.fix_dupe_files = QPushButton(Translations["menu.tools.fix_duplicate_files"]) + self.view_legacy_json_file = QPushButton(open_file_str()) + self.open_backups_folder = QPushButton(Translations["menu.file.open_backups_folder"]) + + self.cleanup_grid_layout.addWidget( + self.fix_unlinked_entries, + self.cleanup_unlinked_row, + self.cleanup_buttons_col, + ) + self.cleanup_grid_layout.addWidget( + self.fix_ignored_entries, + self.cleanup_ignored_row, + self.cleanup_buttons_col, + ) + self.cleanup_grid_layout.addWidget( + self.fix_dupe_files, + self.cleanup_dupe_files_row, + self.cleanup_buttons_col, + ) + self.cleanup_grid_layout.addWidget( + self.view_legacy_json_file, + self.cleanup_legacy_json_row, + self.cleanup_buttons_col, + ) + self.cleanup_grid_layout.addWidget( + self.open_backups_folder, + self.cleanup_backups_row, + self.cleanup_buttons_col, + ) + + self.body_layout.addSpacerItem( + QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + ) + self.body_layout.addWidget(self.cleanup_widget) + self.body_layout.addSpacerItem( + QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + ) + + # Details -------------------------------------------------------------- + self.details_container = QWidget() + self.details_layout = QHBoxLayout(self.details_container) + self.details_layout.setContentsMargins(6, 0, 6, 0) + opacity_effect_details = QGraphicsOpacityEffect(self) + opacity_effect_details.setOpacity(0.5) + + self.version_label = QLabel() + self.version_label.setGraphicsEffect(opacity_effect_details) + self.details_layout.addWidget(self.version_label) + + # Buttons -------------------------------------------------------------- self.button_container = QWidget() self.button_layout = QHBoxLayout(self.button_container) self.button_layout.setContentsMargins(6, 6, 6, 6) @@ -162,4 +461,5 @@ class LibraryInfoWindowView(QWidget): self.root_layout.addWidget(self.body_widget) self.root_layout.addStretch(1) self.root_layout.addStretch(2) + self.root_layout.addWidget(self.details_container) self.root_layout.addWidget(self.button_container) diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index 42cbe9e8..1c5f73d3 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -59,6 +59,7 @@ from tagstudio.core.library.ignore import Ignore from tagstudio.core.media_types import MediaCategories, MediaType from tagstudio.core.palette import UI_COLORS, ColorType, UiColor, get_ui_color from tagstudio.core.utils.encoding import detect_char_encoding +from tagstudio.core.utils.types import unwrap from tagstudio.qt.helpers.blender_thumbnailer import blend_thumb from tagstudio.qt.helpers.color_overlay import theme_fg_overlay from tagstudio.qt.helpers.file_tester import is_readable_video @@ -1436,7 +1437,9 @@ class ThumbRenderer(QObject): if ( image and Ignore.compiled_patterns - and Ignore.compiled_patterns.match(filepath.relative_to(self.lib.library_dir)) + and Ignore.compiled_patterns.match( + filepath.relative_to(unwrap(self.lib.library_dir)) + ) ): image = render_ignored((adj_size, adj_size), pixel_ratio, image) except TypeError: diff --git a/src/tagstudio/resources/qt/images/dupe_file_stat.png b/src/tagstudio/resources/qt/images/dupe_file_stat.png new file mode 100644 index 0000000000000000000000000000000000000000..25381599cb7d5716072ab8d0dd6e55aebf9ff5fb GIT binary patch literal 2973 zcmd5;d0bOh7QP9qAOdRDD%CWWYHW5SkSAf0AfSXIAhIYXBroA%l9%OS!Xgdm)QMUV z6m?p}y)!>`EG`|i+VK^v3L?>|IyxN{6$BL(2j&MeZgZ0$;9zHd^WXgOl6&uW&UeoF z?mH*3CMCxDcun>K0QkhmiId^C#r}A@!_S6r*#!XHZeywGM7ksak>eV+Oo8X1Y_moO zaR9<1%sQETIZDuSP!*;X(Vw0?O{ZZB5q)v6ge%cSqq$gIp#e=POiYy*E|&`x^oVe; zFf#%PG$MvZa< zrXw(1OS5yza`1dYM5jZXHb9P=u_1P?aiDxC1jmkWcx*0bxVbJDCvamft{cqgEuTSn zgd>3|e<6+?8DS3Pdv^r0O!qEhiR3?D!Q?M(K#A{?KGy&DBh}@y@L&#!GF#@oQqRzL!Rg1J3l#%aM^D(h!fr2<+p81Nbp~9KFGm%Q(2kz5qefgw zm}CYtN(FCPM2}J`F{ncUEy>;-G(L^b7Ok@{#5W9KoZ<9N%V84@x;W3^q=X}vGdOeMW^ihw zS~#i=a3&v%oZSQfr3`~zF`^p)m|2luVf#e9aQwN5pj8#q7M8G?AIpOG@bYr@tY2OE zHLpn;)YI%69FhI>;@-xz8?)xCUu1SqtX*_%zCR=5XrLg$Rxt14M@DT&jqT;FqQt%$ z?`zWJ_y>!$ar=&DO6xne`LaU0IM=Z~+b1LSYY%Ps=gXf`kCo41Do^Qu^X;r1&;R^z z>z?nZTQcwX$6P&9zp4JG0~uG_G8zqcaK5OpUjrF{H9ZnT*zPA6O|E9KP+ zyyx6S0~qqaUHewIxA*R?6t?$;-(N6!_k$A|)|+>icDimO*Q{ckeZS!XXy6y=@9bKZ zmU`awy3Mw+Vo8j$Ds1v6XW}>h$jD3F9~*d9T;3E{8S?yf-8Y5mODP9$wXS+Nqrv>h z?fHc90-t+5m$+WB(xTPt2u6evL_gh)Z#h=8GBImZj|=;bQUKyWcJ4%v1ZA6 zW>M)`zhg_HZ{{3Uoqis0J6b3C<3q0%eLFvG31O{tVQy4iFix#0gTJn$ba5F*0KCW9 z9}4)gdJ+IG*W$%dspjg(f4OjAj6UGpv%aY0%GczdsDS}>^~u)^%)QJP9-DtYYinAY zvy1ureax!yKkBVaabr`x$XggIP7mwoT5>u$wsr|vDEK#jHF=d=j&!es7lXp8mM2@%}pBLr7*^gB80y$E3;xoO! z_@v~;f}YSRk^A*a^ti`DrFGdEy$gs<%Ic*G-UFd!nY$;}HUiRLu`ZkZ8Uh;w3n@Q) zf`BOWviJ2g(CRtMnoSBI(7g;Etbhlv7=@IR(Bg+>F&yb)ie-BLo=&$eKA6cYv#wuR zM_n`PNzsUe8R8P zWoFjyY}Yvlo^*kxt>?>k@;xs3TDL&-Wkv3XRF6v>>*v$cue3FamyYS*v7;1~RG+7; z$3N`__qwwzp&qmr%CC*!ZovQB^7vS)d5VYx-yix;K8g~yeyUVjLM^vf`FRJUd*8#B zK-cb>g_hFB!nsv`q@)$?^>Z;WR2^ff+bCn^`SScaO0$F>oBaUk(>k};Pt->Zttm=k zP(Xm&lUU!&Dbp-36~+BuElvhOq@dNcTV&o6eu8?CGWCk^_+9`?z;8=^$Peb-#va?U zsn0Ep)|EFmG4s^bN|CHLuXcR8cLMcM$E&ozk29a`Tz)IMb?LYntYAyQQL^{1O4iKB zEE2g7&Io~iOk}l-U&qjLt1(a~kl$_~Z#8Fh-F`w*eHo4=f+~ErtH7p7J6>I92_X;s z!Ts>06FVry9#6?Ocis0@3}%8BOnj!vT2(oP^lb&*-~K9t8M*xtu$9UszB#;y*`CO` z&Z1Z6+*OCcy(ObgjXvmpNHBYy+oZl4(nM;d(`&7lFMhR&oF&a^HfDL0xP0)p#`R{b S?RWNn)$uWj;zM(#W&Z*y9z5&- literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/ignored_stat.png b/src/tagstudio/resources/qt/images/ignored_stat.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc6d10d77621689e44fe2d1900f95bcb949071f GIT binary patch literal 4184 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 literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/unlinked_stat.png b/src/tagstudio/resources/qt/images/unlinked_stat.png new file mode 100644 index 0000000000000000000000000000000000000000..6fab6133c8c9e5d41195ee4a27eaaecc9812f968 GIT binary patch literal 6287 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 literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 8c63731e..85d47820 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...", "entries.running.dialog.title": "Füge neue Dateieinträge hinzu", "entries.tags": "Tags", - "entries.unlinked.delete": "Unverknüpfte Einträge löschen", - "entries.unlinked.delete.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?", - "entries.unlinked.delete.deleting": "Einträge werden gelöscht", - "entries.unlinked.delete.deleting_count": "Lösche {idx}/{count} unverknüpfte Einträge", - "entries.unlinked.delete_alt": "Unverknüpfte Einträge &löschen", + "entries.remove.plural.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?", + "entries.generic.remove.removing": "Einträge werden gelöscht", "entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.

Nicht verknüpfte Einträge können durch das Durchsuchen Ihrer Verzeichnisse automatisch neu verknüpft, vom Benutzer manuell neu verknüpft oder auf Wunsch gelöscht werden.", - "entries.unlinked.missing_count.none": "Unverknüpfte Einträge: -", - "entries.unlinked.missing_count.some": "Unverknüpfte Einträge: {count}", - "entries.unlinked.refresh_all": "Alle aktualisie&ren", - "entries.unlinked.relink.attempting": "Versuche {idx}/{missing_count} Einträge wieder zu verknüpfen, {fixed_count} bereits erfolgreich wieder verknüpft", + "entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}", + "entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge wieder zu verknüpfen, {fixed_count} bereits erfolgreich wieder verknüpft", "entries.unlinked.relink.manual": "&Manuell Neuverknüpfen", "entries.unlinked.relink.title": "Einträge werden neuverknüpft", "entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 9cf01257..0a9ae6a9 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -40,29 +40,35 @@ "entries.duplicate.merge": "Merge Duplicate Entries", "entries.duplicate.refresh": "Refresh Duplicate Entries", "entries.duplicates.description": "Duplicate entries are defined as multiple entries which point to the same file on disk. Merging these will combine the tags and metadata from all duplicates into a single consolidated entry. These are not to be confused with \"duplicate files\", which are duplicates of your files themselves outside of TagStudio.", + "entries.generic.refresh_alt": "&Refresh", + "entries.generic.remove.removing_count": "Removing {count} Entries...", + "entries.generic.remove.removing": "Removing Entries", + "entries.ignored.description": "File entries are considered to be \"ignored\" if they were added to the library before the user's ignore rules (via the '.ts_ignore' file) were updated to exclude it. Ignored files are kept in the library by default in order to prevent accidental data loss when updating ignore rules.", + "entries.ignored.ignored_count": "Ignored Entries: {count}", + "entries.ignored.remove_alt": "Remo&ve Ignored Entries", + "entries.ignored.remove": "Remove Ignored Entries", + "entries.ignored.scanning": "Scanning Library for Ignored Entries...", + "entries.ignored.title": "Fix Ignored Entries", "entries.mirror.confirmation": "Are you sure you want to mirror the following {count} Entries?", "entries.mirror.label": "Mirroring {idx}/{total} Entries...", "entries.mirror.title": "Mirroring Entries", "entries.mirror.window_title": "Mirror Entries", "entries.mirror": "&Mirror", + "entries.remove.plural.confirm": "Are you sure you want to remove these {count} entries from your library? No files on disk will be deleted.", + "entries.remove.singular.confirm": "Are you sure you want to remove this entry from your library? No files on disk will be deleted.", "entries.running.dialog.new_entries": "Adding {total} New File Entries...", "entries.running.dialog.title": "Adding New File Entries", "entries.tags": "Tags", - "entries.unlinked.delete_alt": "De&lete Unlinked Entries", - "entries.unlinked.delete.confirm": "Are you sure you want to delete the following {count} entries?", - "entries.unlinked.delete.deleting_count": "Deleting {idx}/{count} Unlinked Entries", - "entries.unlinked.delete.deleting": "Deleting Entries", - "entries.unlinked.delete": "Delete Unlinked Entries", "entries.unlinked.description": "Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked.

Unlinked entries may be automatically relinked via searching your directories or deleted if desired.", - "entries.unlinked.missing_count.none": "Unlinked Entries: N/A", - "entries.unlinked.missing_count.some": "Unlinked Entries: {count}", - "entries.unlinked.refresh_all": "&Refresh All", - "entries.unlinked.relink.attempting": "Attempting to Relink {idx}/{missing_count} Entries, {fixed_count} Successfully Relinked", + "entries.unlinked.relink.attempting": "Attempting to Relink {index}/{unlinked_count} Entries, {fixed_count} Successfully Relinked", "entries.unlinked.relink.manual": "&Manual Relink", "entries.unlinked.relink.title": "Relinking Entries", + "entries.unlinked.remove_alt": "Remo&ve Unlinked Entries", + "entries.unlinked.remove": "Remove Unlinked Entries", "entries.unlinked.scanning": "Scanning Library for Unlinked Entries...", "entries.unlinked.search_and_relink": "&Search && Relink", "entries.unlinked.title": "Fix Unlinked Entries", + "entries.unlinked.unlinked_count": "Unlinked Entries: {count}", "ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Copy Field", @@ -71,7 +77,6 @@ "file.date_added": "Date Added", "file.date_created": "Date Created", "file.date_modified": "Date Modified", - "file.path": "File Path", "file.dimensions": "Dimensions", "file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.", "file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.", @@ -91,6 +96,7 @@ "file.open_location.generic": "Show file in file explorer", "file.open_location.mac": "Reveal in Finder", "file.open_location.windows": "Show in File Explorer", + "file.path": "File Path", "folders_to_tags.close_all": "Close All", "folders_to_tags.converting": "Converting folders to Tags", "folders_to_tags.description": "Creates tags based on your folder structure and applies them to your entries.\n The structure below shows all the tags that will be created and what entries they will be applied to.", @@ -115,17 +121,21 @@ "generic.missing": "Missing", "generic.navigation.back": "Back", "generic.navigation.next": "Next", + "generic.no": "No", "generic.none": "None", "generic.overwrite_alt": "&Overwrite", "generic.overwrite": "Overwrite", "generic.paste": "Paste", "generic.recent_libraries": "Recent Libraries", + "generic.remove_alt": "&Remove", + "generic.remove": "Remove", "generic.rename_alt": "&Rename", "generic.rename": "Rename", "generic.reset": "Reset", "generic.save": "Save", "generic.skip_alt": "&Skip", "generic.skip": "Skip", + "generic.yes": "Yes", "home.search_entries": "Search Entries", "home.search_library": "Search Library", "home.search_tags": "Search Tags", @@ -162,6 +172,12 @@ "json_migration.title.old_lib": "

v9.4 Library

", "json_migration.title": "Save Format Migration: \"{path}\"", "landing.open_create_library": "Open/Create Library {shortcut}", + "library_info.cleanup.backups": "Library Backups:", + "library_info.cleanup.dupe_files": "Duplicate Files:", + "library_info.cleanup.ignored": "Ignored Entries:", + "library_info.cleanup.legacy_json": "Leftover Legacy Library:", + "library_info.cleanup.unlinked": "Unlinked Entries:", + "library_info.cleanup": "Cleanup", "library_info.stats.colors": "Tag Colors:", "library_info.stats.entries": "Entries:", "library_info.stats.fields": "Fields:", @@ -170,6 +186,7 @@ "library_info.stats.tags": "Tags:", "library_info.stats": "Statistics", "library_info.title": "Library '{library_dir}'", + "library_info.version": "Library Format Version: {version}", "library_object.name_required": "Name (Required)", "library_object.name": "Name", "library_object.slug_required": "ID Slug (Required)", @@ -201,6 +218,7 @@ "menu.file.missing_library.message": "The location of the library \"{library}\" cannot be found.", "menu.file.missing_library.title": "Missing Library", "menu.file.new_library": "New Library", + "menu.file.open_backups_folder": "Open Backups Folder", "menu.file.open_create_library": "&Open/Create Library", "menu.file.open_library": "Open Library", "menu.file.open_recent_library": "Open Recent", @@ -214,7 +232,8 @@ "menu.macros": "&Macros", "menu.select": "Select", "menu.settings": "Settings...", - "menu.tools.fix_duplicate_files": "Fix Duplicate &Files", + "menu.tools.fix_duplicate_files": "Fix &Duplicate Files", + "menu.tools.fix_ignored_entries": "Fix &Ignored Entries", "menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries", "menu.tools": "&Tools", "menu.view.decrease_thumbnail_size": "Decrease Thumbnail Size", @@ -236,34 +255,34 @@ "select.clear": "Clear Selection", "select.inverse": "Invert Selection", "settings.clear_thumb_cache.title": "Clear Thumbnail Cache", + "settings.dateformat.english": "English", + "settings.dateformat.international": "International", + "settings.dateformat.label": "Date Format", + "settings.dateformat.system": "System", "settings.filepath.label": "Filepath Visibility", "settings.filepath.option.full": "Show Full Paths", "settings.filepath.option.name": "Show Filenames Only", "settings.filepath.option.relative": "Show Relative Paths", + "settings.generate_thumbs": "Thumbnail Generation", "settings.global": "Global Settings", + "settings.hourformat.label": "24-Hour Time", "settings.language": "Language", "settings.library": "Library Settings", "settings.open_library_on_start": "Open Library on Start", - "settings.generate_thumbs": "Thumbnail Generation", "settings.page_size": "Page Size", "settings.restart_required": "Please restart TagStudio for changes to take effect.", "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", - "settings.tag_click_action.label": "Tag Click Action", "settings.tag_click_action.add_to_search": "Add Tag to Search", + "settings.tag_click_action.label": "Tag Click Action", "settings.tag_click_action.open_edit": "Edit Tag", "settings.tag_click_action.set_search": "Search for Tag", "settings.theme.dark": "Dark", "settings.theme.label": "Theme:", "settings.theme.light": "Light", "settings.theme.system": "System", - "settings.dateformat.label": "Date Format", - "settings.dateformat.system": "System", - "settings.dateformat.english": "English", - "settings.dateformat.international": "International", - "settings.hourformat.label": "24-Hour Time", - "settings.zeropadding.label": "Date Zero-Padding", "settings.title": "Settings", + "settings.zeropadding.label": "Date Zero-Padding", "sorting.direction.ascending": "Ascending", "sorting.direction.descending": "Descending", "sorting.mode.random": "Random", diff --git a/src/tagstudio/resources/translations/es.json b/src/tagstudio/resources/translations/es.json index a94b6849..d51768c3 100644 --- a/src/tagstudio/resources/translations/es.json +++ b/src/tagstudio/resources/translations/es.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Añadiendo {total} nuevas entradas de archivos...", "entries.running.dialog.title": "Añadiendo las nuevas entradas de archivos", "entries.tags": "Etiquetas", - "entries.unlinked.delete": "Eliminar entradas no vinculadas", - "entries.unlinked.delete.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?", - "entries.unlinked.delete.deleting": "Eliminando entradas", - "entries.unlinked.delete.deleting_count": "Eliminando {idx}/{count} entradas no vinculadas", - "entries.unlinked.delete_alt": "Eliminar entradas no vinculadas", + "entries.remove.plural.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?", + "entries.generic.remove.removing": "Eliminando entradas", "entries.unlinked.description": "Cada entrada de la biblioteca está vinculada a un archivo en uno de tus directorios. Si un archivo vinculado a una entrada se mueve o se elimina fuera de TagStudio, se considerará desvinculado.

Las entradas no vinculadas se pueden volver a vincular automáticamente mediante una búsqueda en tus directorios, el usuario puede eliminarlas si así lo desea.", - "entries.unlinked.missing_count.none": "Entradas no vinculadas: N/A", - "entries.unlinked.missing_count.some": "Entradas no vinculadas: {count}", - "entries.unlinked.refresh_all": "&Recargar todo", - "entries.unlinked.relink.attempting": "Intentando volver a vincular {idx}/{missing_count} Entradas, {fixed_count} Reenlazado correctamente", + "entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}", + "entries.unlinked.relink.attempting": "Intentando volver a vincular {index}/{unlinked_count} Entradas, {fixed_count} Reenlazado correctamente", "entries.unlinked.relink.manual": "&Reenlace manual", "entries.unlinked.relink.title": "Volver a vincular las entradas", "entries.unlinked.scanning": "Buscando entradas no enlazadas en la biblioteca...", diff --git a/src/tagstudio/resources/translations/fil.json b/src/tagstudio/resources/translations/fil.json index 1fc271b0..65ad70de 100644 --- a/src/tagstudio/resources/translations/fil.json +++ b/src/tagstudio/resources/translations/fil.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Dinadagdag ang {total} Mga Bagong Entry ng File…", "entries.running.dialog.title": "Dinadagdag ang Mga Bagong Entry ng File", "entries.tags": "Mga Tag", - "entries.unlinked.delete": "Burahin ang Mga Hindi Naka-link na Entry", - "entries.unlinked.delete.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?", - "entries.unlinked.delete.deleting": "Binubura ang Mga Entry", - "entries.unlinked.delete.deleting_count": "Binubura ang {idx}/{count} (mga) Naka-unlink na Entry", - "entries.unlinked.delete_alt": "Burahin ang Mga Naka-unlink na Entry", + "entries.remove.plural.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?", + "entries.generic.remove.removing": "Binubura ang Mga Entry", "entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.

Ang mga naka-unlink na entry ay maaring i-link muli sa pamamagitan ng paghahanap sa iyong mga direktoryo o buburahin kung ninanais.", - "entries.unlinked.missing_count.none": "Mga Naka-unlink na Entry: N/A", - "entries.unlinked.missing_count.some": "Mga Naka-unlink na Entry: {count}", - "entries.unlinked.refresh_all": "&I-refresh Lahat", - "entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {idx}/{missing_count} Mga Entry, {fixed_count} Matagumpay na na-link muli", + "entries.unlinked.unlinked_count": "Mga Naka-unlink na Entry: {count}", + "entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {index}/{unlinked_count} Mga Entry, {fixed_count} Matagumpay na na-link muli", "entries.unlinked.relink.manual": "&Manwal na Pag-link Muli", "entries.unlinked.relink.title": "Nili-link muli ang Mga Entry", "entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…", diff --git a/src/tagstudio/resources/translations/fr.json b/src/tagstudio/resources/translations/fr.json index 7deacf58..f6b1a6ef 100644 --- a/src/tagstudio/resources/translations/fr.json +++ b/src/tagstudio/resources/translations/fr.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...", "entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier", "entries.tags": "Tags", - "entries.unlinked.delete": "Supprimer les Entrées non Liées", - "entries.unlinked.delete.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?", - "entries.unlinked.delete.deleting": "Suppression des Entrées", - "entries.unlinked.delete.deleting_count": "Suppression des Entrées non Liées {idx}/{count}", - "entries.unlinked.delete_alt": "Supprimer les Entrées non liées", + "entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?", + "entries.generic.remove.removing": "Suppression des Entrées", "entries.unlinked.description": "Chaque entrée dans la bibliothèque est liée à un fichier dans l'un de vos dossiers. Si un fichier lié à une entrée est déplacé ou supprimé en dehors de TagStudio, il est alors considéré non lié.

Les entrées non liées peuvent être automatiquement reliées via la recherche dans vos dossiers, reliées manuellement par l'utilisateur, ou supprimées si désiré.", - "entries.unlinked.missing_count.none": "Entrées non Liées : N/A", - "entries.unlinked.missing_count.some": "Entrées non Liées : {count}", - "entries.unlinked.refresh_all": "&Tout Rafraîchir", - "entries.unlinked.relink.attempting": "Tentative de Reliage de {idx}/{missing_count} Entrées, {fixed_count} ont été Reliées avec Succès", + "entries.unlinked.unlinked_count": "Entrées non Liées : {count}", + "entries.unlinked.relink.attempting": "Tentative de Reliage de {index}/{unlinked_count} Entrées, {fixed_count} ont été Reliées avec Succès", "entries.unlinked.relink.manual": "&Reliage Manuel", "entries.unlinked.relink.title": "Reliage des Entrées", "entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...", diff --git a/src/tagstudio/resources/translations/hu.json b/src/tagstudio/resources/translations/hu.json index bd0d307c..9c504a20 100644 --- a/src/tagstudio/resources/translations/hu.json +++ b/src/tagstudio/resources/translations/hu.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "{total} új elem felvétele folyamatban…", "entries.running.dialog.title": "Új elemek felvétele", "entries.tags": "Címkék", - "entries.unlinked.delete": "Kapcsolat nélküli elemek törlése", - "entries.unlinked.delete.confirm": "Biztosan törölni akarja az alábbi {count} elemet?", - "entries.unlinked.delete.deleting": "Elemek törlése", - "entries.unlinked.delete.deleting_count": "{count}/{idx}. kapcsolat nélküli elem törlése folyamatban…", - "entries.unlinked.delete_alt": "&Kapcsolat nélküli elemek törlése", + "entries.remove.plural.confirm": "Biztosan törölni akarja az alábbi {count} elemet?", + "entries.generic.remove.removing": "Elemek törlése", "entries.unlinked.description": "A könyvtár minden eleme egy fájllal van összekapcsolva a számítógépen. Ha egy kapcsolt fájl a TagSudión kívül áthelyezésre vagy törésre kerül, akkor ez a kapcsolat megszakad.

Ezeket a kapcsolat nélküli elemeket a program megpróbálhatja automatikusan megkeresni, de Ön is kézileg újra összekapcsolhatja vagy törölheti őket.", - "entries.unlinked.missing_count.none": "Kapcsolat nélküli elemek: 0", - "entries.unlinked.missing_count.some": "Kapcsolat nélküli elemek: {count}", - "entries.unlinked.refresh_all": "&Az összes frissítése", - "entries.unlinked.relink.attempting": "{missing_count}/{idx} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva", + "entries.unlinked.unlinked_count": "Kapcsolat nélküli elemek: {count}", + "entries.unlinked.relink.attempting": "{unlinked_count}/{index} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva", "entries.unlinked.relink.manual": "Új&ra összekapcsolás kézileg", "entries.unlinked.relink.title": "Elemek újra összekapcsolása", "entries.unlinked.scanning": "Kapcsolat nélküli elemek keresése a könyvtárban…", diff --git a/src/tagstudio/resources/translations/ja.json b/src/tagstudio/resources/translations/ja.json index 1f76be3a..d16715b7 100644 --- a/src/tagstudio/resources/translations/ja.json +++ b/src/tagstudio/resources/translations/ja.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...", "entries.running.dialog.title": "新しいファイルエントリを追加", "entries.tags": "タグ", - "entries.unlinked.delete": "未リンクのエントリを削除", - "entries.unlinked.delete.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?", - "entries.unlinked.delete.deleting": "エントリの削除", - "entries.unlinked.delete.deleting_count": "{count} 件中 {idx} 件の未リンクのエントリを削除しています", - "entries.unlinked.delete_alt": "未リンクのエントリを削除(&L)", + "entries.remove.plural.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?", + "entries.generic.remove.removing": "エントリの削除", "entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリは未リンクとして扱われます。

未リンクのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。", - "entries.unlinked.missing_count.none": "未リンクのエントリを削除", - "entries.unlinked.missing_count.some": "未リンクのエントリ数: {count}", - "entries.unlinked.refresh_all": "すべて再読み込み(&R)", - "entries.unlinked.relink.attempting": "{missing_count} 件中 {idx} 件の項目を再リンク中、{fixed_count} 件を正常に再リンクしました", + "entries.unlinked.unlinked_count": "未リンクのエントリ数: {count}", + "entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件の項目を再リンク中、{fixed_count} 件を正常に再リンクしました", "entries.unlinked.relink.manual": "手動で再リンク(&M)", "entries.unlinked.relink.title": "エントリの再リンク", "entries.unlinked.scanning": "未リンクのエントリをライブラリ内でスキャンしています...", diff --git a/src/tagstudio/resources/translations/nb_NO.json b/src/tagstudio/resources/translations/nb_NO.json index d5d818c1..c8e5591d 100644 --- a/src/tagstudio/resources/translations/nb_NO.json +++ b/src/tagstudio/resources/translations/nb_NO.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Legger til {total} Nye Filoppføringer...", "entries.running.dialog.title": "Legger til Nye Filoppføringer", "entries.tags": "Etiketter", - "entries.unlinked.delete": "Slett Frakoblede Oppføringer", - "entries.unlinked.delete.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?", - "entries.unlinked.delete.deleting": "Sletter Oppføringer", - "entries.unlinked.delete.deleting_count": "Sletter {idx}/{count} ulenkede oppføringer", - "entries.unlinked.delete_alt": "&Slett Frakoblede Oppføringer", + "entries.remove.plural.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?", + "entries.generic.remove.removing": "Sletter Oppføringer", "entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.

Frakoblede oppføringer kan bli automatisk gjenkoblet ved å søke i mappene dine eller slettet om det er ønsket.", - "entries.unlinked.missing_count.none": "Frakoblede Oppføringer: N/A", - "entries.unlinked.missing_count.some": "Frakoblede Oppføringer: {count}", - "entries.unlinked.refresh_all": "Gjenoppfrisk alle", - "entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {idx}/{missing_count} Oppføringer, {fixed_count} Klart Gjenkoblet", + "entries.unlinked.unlinked_count": "Frakoblede Oppføringer: {count}", + "entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {index}/{unlinked_count} Oppføringer, {fixed_count} Klart Gjenkoblet", "entries.unlinked.relink.manual": "&Manuell Gjenkobling", "entries.unlinked.relink.title": "Gjenkobler Oppføringer", "entries.unlinked.scanning": "Skanner bibliotek for ulenkede oppføringer …", diff --git a/src/tagstudio/resources/translations/pl.json b/src/tagstudio/resources/translations/pl.json index bdfa9294..59b802b4 100644 --- a/src/tagstudio/resources/translations/pl.json +++ b/src/tagstudio/resources/translations/pl.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Dodawanie {total} nowych wpisów plików...", "entries.running.dialog.title": "Dodawanie nowych wpisów plików", "entries.tags": "Tagi", - "entries.unlinked.delete": "Usuń odłączone wpisy", - "entries.unlinked.delete.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?", - "entries.unlinked.delete.deleting": "Usuwanie wpisów", - "entries.unlinked.delete.deleting_count": "Usuwanie {idx}/{count} odłączonych wpisów", - "entries.unlinked.delete_alt": "Usuń odłączone wpisy", + "entries.remove.plural.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?", + "entries.generic.remove.removing": "Usuwanie wpisów", "entries.unlinked.description": "Każdy wpis w bibliotece jest połączony z plikiem w jednym z twoich katalogów. Jeśli połączony plik jest przeniesiony poza TagStudio albo usunięty to jest uważany za odłączony.

Odłączone wpisy mogą być automatycznie połączone ponownie przez szukanie twoich katalogów, ręczne ponowne łączenie przez użytkownika lub usunięte jeśli zajdzie taka potrzeba.", - "entries.unlinked.missing_count.none": "Odłączone wpisy: brak", - "entries.unlinked.missing_count.some": "Odłączone wpisy: {count}", - "entries.unlinked.refresh_all": "&Odśwież wszystko", - "entries.unlinked.relink.attempting": "Próbowanie ponownego łączenia {idx}/{missing_count} wpisów, {fixed_count} poprawnie połączono ponownie", + "entries.unlinked.unlinked_count": "Odłączone wpisy: {count}", + "entries.unlinked.relink.attempting": "Próbowanie ponownego łączenia {index}/{unlinked_count} wpisów, {fixed_count} poprawnie połączono ponownie", "entries.unlinked.relink.manual": "&Ręczne ponowne łączenie", "entries.unlinked.relink.title": "Ponowne łączenie wpisów", "entries.unlinked.scanning": "Skanowanie biblioteki dla odłączonych wpisów...", diff --git a/src/tagstudio/resources/translations/pt.json b/src/tagstudio/resources/translations/pt.json index 4633fe77..80249285 100644 --- a/src/tagstudio/resources/translations/pt.json +++ b/src/tagstudio/resources/translations/pt.json @@ -47,16 +47,11 @@ "entries.running.dialog.new_entries": "A Adicionar {total} Novos Registos de Ficheiros...", "entries.running.dialog.title": "A Adicionar Novos Registos de Ficheiros", "entries.tags": "Tags", - "entries.unlinked.delete": "Apagar Registos não Referenciados", - "entries.unlinked.delete.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?", - "entries.unlinked.delete.deleting": "A Apagar Registos", - "entries.unlinked.delete.deleting_count": "A Apagar {idx}/{count} Registos Não Ligadas", - "entries.unlinked.delete_alt": "De&letar Registos Não Ligados", + "entries.remove.plural.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?", + "entries.generic.remove.removing": "A Apagar Registos", "entries.unlinked.description": "Cada registo na biblioteca faz referência à um ficheiro numa das suas pastas. Se um ficheiro referenciado à uma entrada for movido ou apagado fora do TagStudio, ele é depois considerado não-referenciado.

Registos não-referenciados podem ser automaticamente referenciados por pesquisas nos seus diretórios, manualmente pelo utilizador, ou apagado se for desejado.", - "entries.unlinked.missing_count.none": "Registos Não Referenciados: N/A", - "entries.unlinked.missing_count.some": "Registos não referenciados: {count}", - "entries.unlinked.refresh_all": "&Atualizar Tudo", - "entries.unlinked.relink.attempting": "A tentar referenciar {idx}/{missing_count} Registos, {fixed_count} Referenciados com Sucesso", + "entries.unlinked.unlinked_count": "Registos Não Referenciados: {count}", + "entries.unlinked.relink.attempting": "A tentar referenciar {index}/{unlinked_count} Registos, {fixed_count} Referenciados com Sucesso", "entries.unlinked.relink.manual": "&Referência Manual", "entries.unlinked.relink.title": "A Referenciar Registos", "entries.unlinked.scanning": "A escanear biblioteca por registos não referenciados...", diff --git a/src/tagstudio/resources/translations/pt_BR.json b/src/tagstudio/resources/translations/pt_BR.json index 98389fe4..71845892 100644 --- a/src/tagstudio/resources/translations/pt_BR.json +++ b/src/tagstudio/resources/translations/pt_BR.json @@ -45,16 +45,11 @@ "entries.running.dialog.new_entries": "Adicionando {total} Novos Registros de Arquivos...", "entries.running.dialog.title": "Adicionando Novos Registros de Arquivos", "entries.tags": "Tags", - "entries.unlinked.delete": "Deletar Registros não Referenciados", - "entries.unlinked.delete.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?", - "entries.unlinked.delete.deleting": "Deletando Registros", - "entries.unlinked.delete.deleting_count": "Deletando {idx}/{count} Registros Não Linkadas", - "entries.unlinked.delete_alt": "De&letar Registros Não Linkados", + "entries.remove.plural.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?", + "entries.generic.remove.removing": "Deletando Registros", "entries.unlinked.description": "Cada registro na biblioteca faz referência à um arquivo em uma de suas pastas. Se um arquivo referenciado à uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não-referenciado.

Registros não-referenciados podem ser automaticamente referenciados por buscas nos seus diretórios, manualmente pelo usuário, ou deletado se for desejado.", - "entries.unlinked.missing_count.none": "Registros Não Referenciados: N/A", - "entries.unlinked.missing_count.some": "Registros não referenciados: {count}", - "entries.unlinked.refresh_all": "&Atualizar Tudo", - "entries.unlinked.relink.attempting": "Tentando referenciar {idx}/{missing_count} Registros, {fixed_count} Referenciados com Sucesso", + "entries.unlinked.unlinked_count": "Registros Não Referenciados: {count}", + "entries.unlinked.relink.attempting": "Tentando referenciar {index}/{unlinked_count} Registros, {fixed_count} Referenciados com Sucesso", "entries.unlinked.relink.manual": "&Referência Manual", "entries.unlinked.relink.title": "Referenciando Registros", "entries.unlinked.scanning": "Escaneando bibliotecada em busca de registros não referenciados...", diff --git a/src/tagstudio/resources/translations/qpv.json b/src/tagstudio/resources/translations/qpv.json index c6a73ca0..07b54b31 100644 --- a/src/tagstudio/resources/translations/qpv.json +++ b/src/tagstudio/resources/translations/qpv.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...", "entries.running.dialog.title": "Nasii neo shiruzmakaban fu mlafu ima", "entries.tags": "Festaretol", - "entries.unlinked.delete": "Keste tsunaganaijena shiruzmakaban", - "entries.unlinked.delete.confirm": "Du kestetsa afto {count} shiruzmakaban?", - "entries.unlinked.delete.deleting": "Keste shiruzmakaban ima", - "entries.unlinked.delete.deleting_count": "Keste {idx}/{count} tsunaganaijena shiruzmakaban ima", - "entries.unlinked.delete_alt": "Ke&ste tsunaganaijena shiruzmakaban", + "entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?", + "entries.generic.remove.removing": "Keste shiruzmakaban ima", "entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore tsunaganaijena.

Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.", - "entries.unlinked.missing_count.none": "Tsunaganaijena shiruzmakaban: Jamnai", - "entries.unlinked.missing_count.some": "Tsunaganaijena shiruzmakaban: {count}", - "entries.unlinked.refresh_all": "&Gotova al gen", - "entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {idx}/{missing_count} shiruzmakaban, {fixed_count} tsunagajena gen", + "entries.unlinked.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}", + "entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen", "entries.unlinked.relink.manual": "&Tsunaga gen mit hant", "entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen", "entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...", diff --git a/src/tagstudio/resources/translations/ru.json b/src/tagstudio/resources/translations/ru.json index bbce81eb..42985657 100644 --- a/src/tagstudio/resources/translations/ru.json +++ b/src/tagstudio/resources/translations/ru.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "Добавление {total} новых записей...", "entries.running.dialog.title": "Добавление новых записей", "entries.tags": "Теги", - "entries.unlinked.delete": "Удалить откреплённые записи", - "entries.unlinked.delete.confirm": "Вы уверены, что хотите удалить {count} записей?", - "entries.unlinked.delete.deleting": "Удаление записей", - "entries.unlinked.delete.deleting_count": "Удаление {idx}/{count} откреплённых записей", - "entries.unlinked.delete_alt": "&Удалить откреплённые записи", + "entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?", + "entries.generic.remove.removing": "Удаление записей", "entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".

Откреплённые записи могут быть прикреплены обратно автоматически, либо же удалены если в них нет надобности.", - "entries.unlinked.missing_count.none": "Откреплённых записей: нет", - "entries.unlinked.missing_count.some": "Откреплённых записей: {count}", - "entries.unlinked.refresh_all": "&Обновить Всё", - "entries.unlinked.relink.attempting": "Попытка перепривязать {idx}/{missing_count} записей, {fixed_count} привязано успешно", + "entries.unlinked.unlinked_count": "Откреплённых записей: {count}", + "entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно", "entries.unlinked.relink.manual": "&Ручная привязка", "entries.unlinked.relink.title": "Привязка записей", "entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...", diff --git a/src/tagstudio/resources/translations/sv.json b/src/tagstudio/resources/translations/sv.json index 5d81b899..5c5ce50c 100644 --- a/src/tagstudio/resources/translations/sv.json +++ b/src/tagstudio/resources/translations/sv.json @@ -26,13 +26,9 @@ "entries.mirror.title": "Speglar Poster", "entries.mirror.window_title": "Spegla Poster", "entries.tags": "Etiketter", - "entries.unlinked.delete": "Radera olänkade poster", - "entries.unlinked.delete.confirm": "Är du säker att du vill radera följande {count} poster?", - "entries.unlinked.delete.deleting": "Raderar poster", - "entries.unlinked.delete.deleting_count": "Raderar {idx}/{count} olänkade poster", - "entries.unlinked.delete_alt": "Radera olänkade poster", + "entries.remove.plural.confirm": "Är du säker att du vill radera följande {count} poster?", + "entries.generic.remove.removing": "Raderar poster", "entries.unlinked.description": "Varje post i biblioteket är länkad till en fil i en av dina kataloger. Om en fil länkad till en post är flyttad eller borttagen utanför TagStudio blir den olänkad. Olänkade poster kan automatiskt bli omlänkade genom att söka genom dina kataloger, manuellt omlänkade av användaren eller tas bort om så önskas.", - "entries.unlinked.refresh_all": "Uppdatera alla", "entries.unlinked.relink.manual": "Länka om manuellt", "entries.unlinked.relink.title": "Länkar om poster", "entries.unlinked.scanning": "Skannar bibliotek efter olänkade poster...", diff --git a/src/tagstudio/resources/translations/ta.json b/src/tagstudio/resources/translations/ta.json index 00d2bd9b..75ac46b3 100644 --- a/src/tagstudio/resources/translations/ta.json +++ b/src/tagstudio/resources/translations/ta.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "{total} புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது ...", "entries.running.dialog.title": "புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது", "entries.tags": "குறிச்சொற்கள்", - "entries.unlinked.delete": "இணைக்கப்படாத உள்ளீடுகளை நீக்கு", - "entries.unlinked.delete.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?", - "entries.unlinked.delete.deleting": "உள்ளீடுகள் நீக்கப்படுகிறது", - "entries.unlinked.delete.deleting_count": "{idx}/{count} இணைக்கப்படாத உள்ளீடுகள் நீக்கப்படுகிறது", - "entries.unlinked.delete_alt": "டி & லெட் இணைக்கப்படாத உள்ளீடுகள்", + "entries.remove.plural.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?", + "entries.generic.remove.removing": "உள்ளீடுகள் நீக்கப்படுகிறது", "entries.unlinked.description": "ஒவ்வொரு நூலக நுழைவும் உங்கள் கோப்பகங்களில் ஒன்றில் ஒரு கோப்போடு இணைக்கப்பட்டுள்ளது. ஒரு நுழைவுடன் இணைக்கப்பட்ட ஒரு கோப்பு டாக்ச்டுடியோவுக்கு வெளியே நகர்த்தப்பட்டால் அல்லது நீக்கப்பட்டால், அது பின்னர் இணைக்கப்படாததாகக் கருதப்படுகிறது.", - "entries.unlinked.missing_count.none": "இணைக்கப்படாத உள்ளீடுகள்: இதற்கில்லை", - "entries.unlinked.missing_count.some": "இணைக்கப்படாத உள்ளீடுகள்: {count}", - "entries.unlinked.refresh_all": "& அனைத்தையும் புதுப்பிக்கவும்", - "entries.unlinked.relink.attempting": "{idx}/{missing_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது", + "entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}", + "entries.unlinked.relink.attempting": "{index}/{unlinked_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது", "entries.unlinked.relink.manual": "& கையேடு மறுபரிசீலனை", "entries.unlinked.relink.title": "உள்ளீடுகள் மீண்டும் இணைக்கப்படுகின்றது", "entries.unlinked.scanning": "இணைக்கப்படாத நுழைவுகளை புத்தககல்லரியில் சோதனை செய்யப்படுகிறது...", diff --git a/src/tagstudio/resources/translations/tok.json b/src/tagstudio/resources/translations/tok.json index 79831895..c9657c60 100644 --- a/src/tagstudio/resources/translations/tok.json +++ b/src/tagstudio/resources/translations/tok.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "mi pana e lipu sin {total}...", "entries.running.dialog.title": "mi pana e lipu sin", "entries.tags": "poki", - "entries.unlinked.delete": "o weka e ijo pi ijo lon ala", - "entries.unlinked.delete.confirm": "mi weka e ijo {count}. ni li pona anu seme?", - "entries.unlinked.delete.deleting": "mi weka e ijo", - "entries.unlinked.delete.deleting_count": "mi weka e ijo {idx}/{count} pi ijo lon ala", - "entries.unlinked.delete_alt": "o weka e ijo pi &linja ala", + "entries.remove.plural.confirm": "mi weka e ijo {count}. ni li pona anu seme?", + "entries.generic.remove.removing": "mi weka e ijo", "entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.

ijo pi ijo lon li ken alasa lon tomo li ken kama jo e ijo lon. ante la sina ken weka e ona.", - "entries.unlinked.missing_count.none": "ijo pi ijo lon ala: N/A", - "entries.unlinked.missing_count.some": "ijo pi ijo lon ala: {count}", - "entries.unlinked.refresh_all": "o lukin sin e ale (&R)", - "entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {idx}/{missing_count}. mi pana e ijo lon tawa ijo {fixed_count}", + "entries.unlinked.unlinked_count": "ijo pi ijo lon ala: {count}", + "entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {index}/{unlinked_count}. mi pana e ijo lon tawa ijo {fixed_count}", "entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo (&M)", "entries.unlinked.relink.title": "mi pana e ijo lon tawa ijo", "entries.unlinked.scanning": "mi o alasa e ijo pi ijo lon ala...", diff --git a/src/tagstudio/resources/translations/tr.json b/src/tagstudio/resources/translations/tr.json index a775143d..bb83d6da 100644 --- a/src/tagstudio/resources/translations/tr.json +++ b/src/tagstudio/resources/translations/tr.json @@ -47,16 +47,11 @@ "entries.running.dialog.new_entries": "{total} Yeni Dosya Kaydı Ekleniyor...", "entries.running.dialog.title": "Yeni Dosya Kayıtları Ekleniyor", "entries.tags": "Etiketler", - "entries.unlinked.delete": "Kopmuş Kayıtları Sil", - "entries.unlinked.delete.confirm": "{count} tane kayıtları silmek istediğinden emin misin?", - "entries.unlinked.delete.deleting": "Kayıtlar Siliniyor", - "entries.unlinked.delete.deleting_count": "{idx}/{count} Kopmuş Kayıt Siliniyor", - "entries.unlinked.delete_alt": "Kopmuş Kayıtları S&il", + "entries.remove.plural.confirm": "{count} tane kayıtları silmek istediğinden emin misin?", + "entries.generic.remove.removing": "Kayıtlar Siliniyor", "entries.unlinked.description": "Kütüphanenizdeki her bir kayıt, dizinlerinizden bir tane dosya ile eşleştirilmektedir. Eğer bir kayıta bağlı dosya TagStudio dışında taşınır veya silinirse, o dosya artık kopmuş olarak sayılır.

Kopmuş kayıtlar dizinlerinizde arama yapılırken otomatik olarak tekrar eşleştirilebilir, manuel olarak sizin tarafınızdan eşleştirilebilir veya isteğiniz üzere silinebilir.", - "entries.unlinked.missing_count.none": "Kopmuş Kayıtlar: Yok", - "entries.unlinked.missing_count.some": "Kopmuş Kayıtlar: {count}", - "entries.unlinked.refresh_all": "&Tümünü Yenile", - "entries.unlinked.relink.attempting": "{idx}/{missing_count} Kayıt Yeniden Eşleştirilmeye Çalışılıyor, {fixed_count} Başarıyla Yeniden Eşleştirildi", + "entries.unlinked.unlinked_count": "Kopmuş Kayıtlar: {count}", + "entries.unlinked.relink.attempting": "{index}/{unlinked_count} Kayıt Yeniden Eşleştirilmeye Çalışılıyor, {fixed_count} Başarıyla Yeniden Eşleştirildi", "entries.unlinked.relink.manual": "&Manuel Yeniden Eşleştirme", "entries.unlinked.relink.title": "Kayıtlar Yeniden Eşleştiriliyor", "entries.unlinked.scanning": "Kütüphane, Kopmuş Kayıtlar için Taranıyor...", diff --git a/src/tagstudio/resources/translations/zh_Hans.json b/src/tagstudio/resources/translations/zh_Hans.json index 0b2fc65e..e398b694 100644 --- a/src/tagstudio/resources/translations/zh_Hans.json +++ b/src/tagstudio/resources/translations/zh_Hans.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "正在加入 {total} 个新文件项目...", "entries.running.dialog.title": "正在加入新文件项目", "entries.tags": "标签", - "entries.unlinked.delete": "删除未链接的项目", - "entries.unlinked.delete.confirm": "您确定要删除以下 {count} 个项目?", - "entries.unlinked.delete.deleting": "正在删除项目", - "entries.unlinked.delete.deleting_count": "正在删除 {idx}/{count} 个未链接的项目", - "entries.unlinked.delete_alt": "删除未链接的项目(&l)", + "entries.remove.plural.confirm": "您确定要删除以下 {count} 个项目?", + "entries.generic.remove.removing": "正在删除项目", "entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。

未链接的条目可能会通过搜索目录自动重新链接,或者根据需要删除。", - "entries.unlinked.missing_count.none": "未链接的项目: 无", - "entries.unlinked.missing_count.some": "未链接的项目: {count}", - "entries.unlinked.refresh_all": "全部刷新(&r)", - "entries.unlinked.relink.attempting": "正在尝试重新链接 {idx}/{missing_count} 个项目, {fixed_count} 个项目成功重链", + "entries.unlinked.unlinked_count": "未链接的项目: {count}", + "entries.unlinked.relink.attempting": "正在尝试重新链接 {index}/{unlinked_count} 个项目, {fixed_count} 个项目成功重链", "entries.unlinked.relink.manual": "手动重新链接(&m)", "entries.unlinked.relink.title": "正在重新链接项目", "entries.unlinked.scanning": "正在扫描仓库以寻找未链接的项目...", diff --git a/src/tagstudio/resources/translations/zh_Hant.json b/src/tagstudio/resources/translations/zh_Hant.json index cfa6eb93..e7394505 100644 --- a/src/tagstudio/resources/translations/zh_Hant.json +++ b/src/tagstudio/resources/translations/zh_Hant.json @@ -48,16 +48,11 @@ "entries.running.dialog.new_entries": "正在加入 {total} 個新檔案項目...", "entries.running.dialog.title": "正在加入新檔案項目", "entries.tags": "標籤", - "entries.unlinked.delete_alt": "刪除未連接項目", - "entries.unlinked.delete.confirm": "您確定要刪除 {count} 個項目嗎?", - "entries.unlinked.delete.deleting_count": "正在刪除 {idx}/{count} 個未連接項目", - "entries.unlinked.delete.deleting": "正在刪除項目", - "entries.unlinked.delete": "刪除未連接項目", + "entries.remove.plural.confirm": "您確定要刪除 {count} 個項目嗎?", + "entries.generic.remove.removing": "正在刪除項目", "entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。

您可以透過搜尋您的檔案來讓未連接的項目自動重新連接,或者自動刪除這些未連接項目。", - "entries.unlinked.missing_count.none": "未連接項目:無", - "entries.unlinked.missing_count.some": "未連接項目:{count}", - "entries.unlinked.refresh_all": "重新整理全部 (&R)", - "entries.unlinked.relink.attempting": "正在嘗試重新連接 {idx}/{missing_count} 個項目,已成功重新連接 {fixed_count} 個", + "entries.unlinked.unlinked_count": "未連接項目:{count}", + "entries.unlinked.relink.attempting": "正在嘗試重新連接 {index}/{unlinked_count} 個項目,已成功重新連接 {fixed_count} 個", "entries.unlinked.relink.manual": "手動重新連接 (&M)", "entries.unlinked.relink.title": "正在重新連接", "entries.unlinked.scanning": "正在掃描文件庫中的未連接項目...", diff --git a/tests/macros/test_missing_files.py b/tests/macros/test_missing_files.py index 20b34f5c..9734a133 100644 --- a/tests/macros/test_missing_files.py +++ b/tests/macros/test_missing_files.py @@ -9,8 +9,8 @@ import pytest from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library -from tagstudio.core.utils.missing_files import MissingRegistry from tagstudio.core.utils.types import unwrap +from tagstudio.core.utils.unlinked_registry import UnlinkedRegistry CWD = Path(__file__).parent @@ -18,16 +18,16 @@ CWD = Path(__file__).parent # NOTE: Does this test actually work? @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True) def test_refresh_missing_files(library: Library): - registry = MissingRegistry(library=library) + registry = UnlinkedRegistry(lib=library) # touch the file `one/two/bar.md` but in wrong location to simulate a moved file (unwrap(library.library_dir) / "bar.md").touch() # no files actually exist, so it should return all entries - assert list(registry.refresh_missing_files()) == [0, 1] + assert list(registry.refresh_unlinked_files()) == [0, 1] # neither of the library entries exist - assert len(registry.missing_file_entries) == 2 + assert len(registry.unlinked_entries) == 2 # iterate through two files assert list(registry.fix_unlinked_entries()) == [0, 1] diff --git a/tests/qt/test_resource_manager.py b/tests/qt/test_resource_manager.py new file mode 100644 index 00000000..bb9ae72f --- /dev/null +++ b/tests/qt/test_resource_manager.py @@ -0,0 +1,19 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +import structlog + +from tagstudio.core.utils.types import unwrap +from tagstudio.qt.resource_manager import ResourceManager + +logger = structlog.get_logger() + + +def test_get(): + rm = ResourceManager() + + for res in rm._map: # pyright: ignore[reportPrivateUsage] + assert rm.get(res), f"Could not get resource '{res}'" + assert unwrap(rm.get_path(res)).exists(), f"Filepath for resource '{res}' does not exist" diff --git a/tests/test_db_migrations.py b/tests/test_db_migrations.py index 21f7a046..2a1c7c96 100644 --- a/tests/test_db_migrations.py +++ b/tests/test_db_migrations.py @@ -9,6 +9,9 @@ from pathlib import Path import pytest from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.library.alchemy.constants import ( + SQL_FILENAME, +) from tagstudio.core.library.alchemy.library import Library CWD = Path(__file__) @@ -36,8 +39,8 @@ def test_library_migrations(path: str): temp_path_ts = temp_path / TS_FOLDER_NAME temp_path_ts.mkdir(exist_ok=True) shutil.copy( - original_path / TS_FOLDER_NAME / Library.SQL_FILENAME, - temp_path / TS_FOLDER_NAME / Library.SQL_FILENAME, + original_path / TS_FOLDER_NAME / SQL_FILENAME, + temp_path / TS_FOLDER_NAME / SQL_FILENAME, ) try: From 1ae92a3661f7718a1f980cf69bbd0d1733fa710f Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:01:32 -0700 Subject: [PATCH 10/19] feat: add setting to select splash screen (#1077) * feat: add setting to select splash screen * feat: add random setting to splash screens --- src/tagstudio/core/global_settings.py | 32 ++-- src/tagstudio/qt/modals/settings_panel.py | 161 ++++++++++-------- src/tagstudio/qt/resources.json | 4 + src/tagstudio/qt/splash.py | 42 +++-- src/tagstudio/qt/ts_qt.py | 6 +- .../resources/qt/images/splash/95.png | Bin 0 -> 118859 bytes src/tagstudio/resources/translations/en.json | 6 + 7 files changed, 147 insertions(+), 104 deletions(-) create mode 100644 src/tagstudio/resources/qt/images/splash/95.png diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index a1d97c6a..7f3add39 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -3,7 +3,7 @@ import platform from datetime import datetime -from enum import Enum +from enum import Enum, IntEnum, StrEnum from pathlib import Path from typing import override @@ -13,34 +13,41 @@ from pydantic import BaseModel, Field from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption -if platform.system() == "Windows": - DEFAULT_GLOBAL_SETTINGS_PATH = ( - Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" - ) -else: - DEFAULT_GLOBAL_SETTINGS_PATH = Path.home() / ".config" / "TagStudio" / "settings.toml" +DEFAULT_GLOBAL_SETTINGS_PATH = ( + Path.home() / "Appdata" / "Roaming" / "TagStudio" / "settings.toml" + if platform.system() == "Windows" + else Path.home() / ".config" / "TagStudio" / "settings.toml" +) logger = structlog.get_logger(__name__) class TomlEnumEncoder(toml.TomlEncoder): @override - def dump_value(self, v): + def dump_value(self, v): # pyright: ignore[reportMissingParameterType] if isinstance(v, Enum): return super().dump_value(v.value) return super().dump_value(v) -class Theme(Enum): +class Theme(IntEnum): DARK = 0 LIGHT = 1 SYSTEM = 2 DEFAULT = SYSTEM +class Splash(StrEnum): + DEFAULT = "default" + RANDOM = "random" + CLASSIC = "classic" + GOO_GEARS = "goo_gears" + NINETY_FIVE = "95" + + # NOTE: pydantic also has a BaseSettings class (from pydantic-settings) that allows any settings -# properties to be overwritten with environment variables. as tagstudio is not currently using -# environment variables, i did not base it on that, but that may be useful in the future. +# properties to be overwritten with environment variables. As TagStudio is not currently using +# environment variables, this was not based on that, but that may be useful in the future. class GlobalSettings(BaseModel): language: str = Field(default="en") open_last_loaded_on_startup: bool = Field(default=True) @@ -50,8 +57,9 @@ class GlobalSettings(BaseModel): show_filenames_in_grid: bool = Field(default=True) page_size: int = Field(default=100) show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT) - theme: Theme = Field(default=Theme.SYSTEM) tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT) + theme: Theme = Field(default=Theme.SYSTEM) + splash: Splash = Field(default=Splash.DEFAULT) date_format: str = Field(default="%x") hour_format: bool = Field(default=True) diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 5c533256..25f1b6d4 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -3,7 +3,7 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from PySide6.QtCore import Qt from PySide6.QtWidgets import ( @@ -18,65 +18,65 @@ from PySide6.QtWidgets import ( ) from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption -from tagstudio.core.global_settings import Theme +from tagstudio.core.global_settings import Splash, Theme from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations from tagstudio.qt.widgets.panel import PanelModal, PanelWidget if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver -FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = {} - -THEME_MAP: dict[Theme, str] = {} - -TAG_CLICK_ACTION_MAP: dict[TagClickActionOption, str] = {} - -DATE_FORMAT_MAP: dict[str, str] = { - "%d/%m/%y": "21/08/24", - "%d/%m/%Y": "21/08/2024", - "%d.%m.%y": "21.08.24", - "%d.%m.%Y": "21.08.2024", - "%d-%m-%y": "21-08-24", - "%d-%m-%Y": "21-08-2024", - "%x": "08/21/24", - "%m/%d/%Y": "08/21/2024", - "%m-%d-%y": "08-21-24", - "%m-%d-%Y": "08-21-2024", - "%m.%d.%y": "08.21.24", - "%m.%d.%Y": "08.21.2024", - "%Y/%m/%d": "2024/08/21", - "%Y-%m-%d": "2024-08-21", - "%Y.%m.%d": "2024.08.21", -} - class SettingsPanel(PanelWidget): driver: "QtDriver" + filepath_option_map: dict[ShowFilepathOption, str] = { + ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], + ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations["settings.filepath.option.relative"], + ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], + } + + theme_map: dict[Theme, str] = { + Theme.SYSTEM: Translations["settings.theme.system"], + Theme.DARK: Translations["settings.theme.dark"], + Theme.LIGHT: Translations["settings.theme.light"], + } + + splash_map: dict[Splash, str] = { + Splash.DEFAULT: Translations["settings.splash.option.default"], + Splash.RANDOM: Translations["settings.splash.option.random"], + Splash.CLASSIC: Translations["settings.splash.option.classic"], + Splash.GOO_GEARS: Translations["settings.splash.option.goo_gears"], + Splash.NINETY_FIVE: Translations["settings.splash.option.ninety_five"], + } + + tag_click_action_map: dict[TagClickActionOption, str] = { + TagClickActionOption.OPEN_EDIT: Translations["settings.tag_click_action.open_edit"], + TagClickActionOption.SET_SEARCH: Translations["settings.tag_click_action.set_search"], + TagClickActionOption.ADD_TO_SEARCH: Translations["settings.tag_click_action.add_to_search"], + } + + date_format_map: dict[str, str] = { + "%d/%m/%y": "21/08/24", + "%d/%m/%Y": "21/08/2024", + "%d.%m.%y": "21.08.24", + "%d.%m.%Y": "21.08.2024", + "%d-%m-%y": "21-08-24", + "%d-%m-%Y": "21-08-2024", + "%x": "08/21/24", + "%m/%d/%Y": "08/21/2024", + "%m-%d-%y": "08-21-24", + "%m-%d-%Y": "08-21-2024", + "%m.%d.%y": "08.21.24", + "%m.%d.%Y": "08.21.2024", + "%Y/%m/%d": "2024/08/21", + "%Y-%m-%d": "2024-08-21", + "%Y.%m.%d": "2024.08.21", + } + def __init__(self, driver: "QtDriver"): super().__init__() # set these "constants" because language will be loaded from config shortly after startup # and we want to use the current language for the dropdowns - global FILEPATH_OPTION_MAP, THEME_MAP, TAG_CLICK_ACTION_MAP - FILEPATH_OPTION_MAP = { - ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"], - ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations[ - "settings.filepath.option.relative" - ], - ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"], - } - THEME_MAP = { - Theme.DARK: Translations["settings.theme.dark"], - Theme.LIGHT: Translations["settings.theme.light"], - Theme.SYSTEM: Translations["settings.theme.system"], - } - TAG_CLICK_ACTION_MAP = { - TagClickActionOption.OPEN_EDIT: Translations["settings.tag_click_action.open_edit"], - TagClickActionOption.SET_SEARCH: Translations["settings.tag_click_action.set_search"], - TagClickActionOption.ADD_TO_SEARCH: Translations[ - "settings.tag_click_action.add_to_search" - ], - } self.driver = driver self.setMinimumSize(400, 300) @@ -84,6 +84,8 @@ class SettingsPanel(PanelWidget): self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(0, 6, 0, 0) + self.library_settings_container = QWidget() + # Tabs self.tab_widget = QTabWidget() @@ -135,6 +137,7 @@ class SettingsPanel(PanelWidget): Translations["settings.open_library_on_start"], self.open_last_lib_checkbox ) + # Generate Thumbnails self.generate_thumbs = QCheckBox() self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs) form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs) @@ -165,49 +168,61 @@ class SettingsPanel(PanelWidget): # Show Filepath self.filepath_combobox = QComboBox() - for k in FILEPATH_OPTION_MAP: - self.filepath_combobox.addItem(FILEPATH_OPTION_MAP[k], k) + for k in SettingsPanel.filepath_option_map: + self.filepath_combobox.addItem(SettingsPanel.filepath_option_map[k], k) filepath_option: ShowFilepathOption = self.driver.settings.show_filepath - if filepath_option not in FILEPATH_OPTION_MAP: + if filepath_option not in SettingsPanel.filepath_option_map: filepath_option = ShowFilepathOption.DEFAULT self.filepath_combobox.setCurrentIndex( - list(FILEPATH_OPTION_MAP.keys()).index(filepath_option) + list(SettingsPanel.filepath_option_map.keys()).index(filepath_option) ) form_layout.addRow(Translations["settings.filepath.label"], self.filepath_combobox) - # Dark Mode - self.theme_combobox = QComboBox() - for k in THEME_MAP: - self.theme_combobox.addItem(THEME_MAP[k], k) - theme = self.driver.settings.theme - if theme not in THEME_MAP: - theme = Theme.DEFAULT - self.theme_combobox.setCurrentIndex(list(THEME_MAP.keys()).index(theme)) - self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label) - form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox) - # Tag Click Action self.tag_click_action_combobox = QComboBox() - for k in TAG_CLICK_ACTION_MAP: - self.tag_click_action_combobox.addItem(TAG_CLICK_ACTION_MAP[k], k) + for k in SettingsPanel.tag_click_action_map: + self.tag_click_action_combobox.addItem(SettingsPanel.tag_click_action_map[k], k) tag_click_action = self.driver.settings.tag_click_action - if tag_click_action not in TAG_CLICK_ACTION_MAP: + if tag_click_action not in SettingsPanel.tag_click_action_map: tag_click_action = TagClickActionOption.DEFAULT self.tag_click_action_combobox.setCurrentIndex( - list(TAG_CLICK_ACTION_MAP.keys()).index(tag_click_action) + list(SettingsPanel.tag_click_action_map.keys()).index(tag_click_action) ) form_layout.addRow( Translations["settings.tag_click_action.label"], self.tag_click_action_combobox ) + # Dark Mode + self.theme_combobox = QComboBox() + for k in SettingsPanel.theme_map: + self.theme_combobox.addItem(SettingsPanel.theme_map[k], k) + theme = self.driver.settings.theme + if theme not in SettingsPanel.theme_map: + theme = Theme.DEFAULT + self.theme_combobox.setCurrentIndex(list(SettingsPanel.theme_map.keys()).index(theme)) + self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label) + form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox) + + # Splash Screen + self.splash_combobox = QComboBox() + for k in SettingsPanel.splash_map: + self.splash_combobox.addItem(SettingsPanel.splash_map[k], k) + splash = self.driver.settings.splash + if splash not in SettingsPanel.splash_map: + splash = Splash.DEFAULT + self.splash_combobox.setCurrentIndex(list(SettingsPanel.splash_map.keys()).index(splash)) + form_layout.addRow(Translations["settings.splash.label"], self.splash_combobox) + # Date Format self.dateformat_combobox = QComboBox() - for k in DATE_FORMAT_MAP: - self.dateformat_combobox.addItem(DATE_FORMAT_MAP[k], k) + for k in SettingsPanel.date_format_map: + self.dateformat_combobox.addItem(SettingsPanel.date_format_map[k], k) dateformat: str = self.driver.settings.date_format - if dateformat not in DATE_FORMAT_MAP: + if dateformat not in SettingsPanel.date_format_map: dateformat = "%x" - self.dateformat_combobox.setCurrentIndex(list(DATE_FORMAT_MAP.keys()).index(dateformat)) + self.dateformat_combobox.setCurrentIndex( + list(SettingsPanel.date_format_map.keys()).index(dateformat) + ) self.dateformat_combobox.currentIndexChanged.connect(self.__update_restart_label) form_layout.addRow(Translations["settings.dateformat.label"], self.dateformat_combobox) @@ -221,8 +236,8 @@ class SettingsPanel(PanelWidget): self.zeropadding_checkbox.setChecked(self.driver.settings.zero_padding) form_layout.addRow(Translations["settings.zeropadding.label"], self.zeropadding_checkbox) - def __build_library_settings(self): - self.library_settings_container = QWidget() + # TODO: Implement Library Settings + def __build_library_settings(self): # pyright: ignore[reportUnusedFunction] form_layout = QFormLayout(self.library_settings_container) form_layout.setContentsMargins(6, 6, 6, 6) @@ -232,7 +247,7 @@ class SettingsPanel(PanelWidget): def __get_language(self) -> str: return list(LANGUAGES.values())[self.language_combobox.currentIndex()] - def get_settings(self) -> dict: + def get_settings(self) -> dict[str, Any]: # pyright: ignore[reportExplicitAny] return { "language": self.__get_language(), "open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(), @@ -246,6 +261,7 @@ class SettingsPanel(PanelWidget): "date_format": self.dateformat_combobox.currentData(), "hour_format": self.hourformat_checkbox.isChecked(), "zero_padding": self.zeropadding_checkbox.isChecked(), + "splash": self.splash_combobox.currentData(), } def update_settings(self, driver: "QtDriver"): @@ -263,6 +279,7 @@ class SettingsPanel(PanelWidget): driver.settings.date_format = settings["date_format"] driver.settings.hour_format = settings["hour_format"] driver.settings.zero_padding = settings["zero_padding"] + driver.settings.splash = settings["splash"] driver.settings.save() diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 34b46ef7..f7bae77c 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -7,6 +7,10 @@ "path": "qt/images/splash/goo_gears.png", "mode": "qpixmap" }, + "splash_95": { + "path": "qt/images/splash/95.png", + "mode": "qpixmap" + }, "icon": { "path": "icon.png", "mode": "pil" diff --git a/src/tagstudio/qt/splash.py b/src/tagstudio/qt/splash.py index c6b778f7..91ae7f99 100644 --- a/src/tagstudio/qt/splash.py +++ b/src/tagstudio/qt/splash.py @@ -4,6 +4,7 @@ import math +import random import structlog from PySide6.QtCore import QRect, Qt @@ -11,12 +12,13 @@ from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap from PySide6.QtWidgets import QSplashScreen, QWidget from tagstudio.core.constants import VERSION, VERSION_BRANCH +from tagstudio.core.global_settings import Splash from tagstudio.qt.resource_manager import ResourceManager logger = structlog.get_logger(__name__) -class Splash: +class SplashScreen: """The custom splash screen widget for TagStudio.""" COPYRIGHT_YEARS: str = "2021-2025" @@ -24,11 +26,7 @@ class Splash: VERSION_STR: str = ( f"Version {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" ) - - SPLASH_CLASSIC: str = "classic" - SPLASH_GOO_GEARS: str = "goo_gears" - SPLASH_95: str = "95" - DEFAULT_SPLASH: str = SPLASH_GOO_GEARS + DEFAULT_SPLASH = Splash.GOO_GEARS def __init__( self, @@ -41,11 +39,19 @@ class Splash: self.screen_width = screen_width self.ratio: float = device_ratio self.splash_screen: QSplashScreen | None = None - self.splash_name: str = splash_name if splash_name else Splash.DEFAULT_SPLASH + if not splash_name or splash_name == Splash.DEFAULT: + self.splash_name: str = SplashScreen.DEFAULT_SPLASH + elif splash_name == Splash.RANDOM: + splash_list = list(Splash) + splash_list.remove(Splash.DEFAULT) + splash_list.remove(Splash.RANDOM) + self.splash_name = random.choice(splash_list) + else: + self.splash_name = splash_name def get_pixmap(self) -> QPixmap: """Get the pixmap used for the splash screen.""" - pixmap: QPixmap | None = self.rm.get(f"splash_{self.splash_name}") + pixmap: QPixmap | None = self.rm.get(f"splash_{self.splash_name}") # pyright: ignore[reportAssignmentType] if not pixmap: logger.error("[Splash] Splash screen not found:", splash_name=self.splash_name) pixmap = QPixmap(960, 540) @@ -55,10 +61,12 @@ class Splash: match painter.font().family(): case "Segoe UI": point_size_scale = 0.75 + case _: + pass # TODO: Store any differing data elsewhere and load dynamically instead of hardcoding. match self.splash_name: - case Splash.SPLASH_CLASSIC: + case Splash.CLASSIC: # Copyright font = painter.font() font.setPointSize(math.floor(22 * point_size_scale)) @@ -68,7 +76,7 @@ class Splash: painter.drawText( QRect(0, -50, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - Splash.COPYRIGHT_STR, + SplashScreen.COPYRIGHT_STR, ) # Version pen = QPen(QColor("#809782ff")) @@ -76,10 +84,10 @@ class Splash: painter.drawText( QRect(0, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - Splash.VERSION_STR, + SplashScreen.VERSION_STR, ) - case Splash.SPLASH_GOO_GEARS: + case Splash.GOO_GEARS: # Copyright font = painter.font() font.setPointSize(math.floor(22 * point_size_scale)) @@ -88,7 +96,7 @@ class Splash: painter.setPen(pen) painter.drawText( QRect(40, 450, 960, 540), - Splash.COPYRIGHT_STR, + SplashScreen.COPYRIGHT_STR, ) # Version font = painter.font() @@ -98,10 +106,10 @@ class Splash: painter.setPen(pen) painter.drawText( QRect(40, 475, 960, 540), - Splash.VERSION_STR, + SplashScreen.VERSION_STR, ) - case Splash.SPLASH_95: + case Splash.NINETY_FIVE: # Copyright font = QFont() font.setFamily("Times") @@ -114,7 +122,7 @@ class Splash: painter.drawText( QRect(88, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft), - Splash.COPYRIGHT_STR, + SplashScreen.COPYRIGHT_STR, ) # Version font.setPointSize(math.floor(22 * point_size_scale)) @@ -124,7 +132,7 @@ class Splash: painter.drawText( QRect(-30, 25, 960, 540), int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight), - Splash.VERSION_STR, + SplashScreen.VERSION_STR, ) case _: diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 7830bcef..1f63d536 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -91,7 +91,7 @@ from tagstudio.qt.modals.tag_database import TagDatabasePanel from tagstudio.qt.modals.tag_search import TagSearchModal from tagstudio.qt.platform_strings import trash_term from tagstudio.qt.resource_manager import ResourceManager -from tagstudio.qt.splash import Splash +from tagstudio.qt.splash import SplashScreen from tagstudio.qt.translations import Translations from tagstudio.qt.widgets.item_thumb import BadgeType, ItemThumb from tagstudio.qt.widgets.migration_modal import JsonMigrationModal @@ -327,10 +327,10 @@ class QtDriver(DriverMixin, QObject): self.main_window.dragMoveEvent = self.drag_move_event self.main_window.dropEvent = self.drop_event - self.splash: Splash = Splash( + self.splash: SplashScreen = SplashScreen( resource_manager=self.rm, screen_width=QGuiApplication.primaryScreen().geometry().width(), - splash_name="", # TODO: Get splash name from config + splash_name=self.settings.splash, device_ratio=self.main_window.devicePixelRatio(), ) self.splash.show() diff --git a/src/tagstudio/resources/qt/images/splash/95.png b/src/tagstudio/resources/qt/images/splash/95.png new file mode 100644 index 0000000000000000000000000000000000000000..072ae8da3e4ce134c5b393895c666c560592ea04 GIT binary patch literal 118859 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�FXlD5@PWf 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�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 Date: Mon, 1 Sep 2025 21:04:11 +0200 Subject: [PATCH 11/19] translations: update from Hosted Weblate (#1076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Turkish) Currently translated at 80.5% (281 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/ Translation: TagStudio/Strings * Translated using Weblate (Filipino) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: searinminecraft Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/ Translation: TagStudio/Strings * Translated using Weblate (Tamil) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/ Translation: TagStudio/Strings * Translated using Weblate (Portuguese (Brazil)) Currently translated at 71.3% (249 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/ Translation: TagStudio/Strings * Translated using Weblate (German) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/ Translation: TagStudio/Strings * Translated using Weblate (Russian) Currently translated at 89.6% (313 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/ Translation: TagStudio/Strings * Translated using Weblate (Japanese) Currently translated at 89.6% (313 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: wany-oh Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/ Translation: TagStudio/Strings * Translated using Weblate (Portuguese) Currently translated at 73.6% (257 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: ssantos Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt/ Translation: TagStudio/Strings * Translated using Weblate (Hungarian) Currently translated at 92.8% (324 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/ Translation: TagStudio/Strings * Translated using Weblate (Polish) Currently translated at 84.8% (296 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pl/ Translation: TagStudio/Strings * Translated using Weblate (Spanish) Currently translated at 90.2% (315 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/ Translation: TagStudio/Strings * Translated using Weblate (French) Currently translated at 92.8% (324 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/ Translation: TagStudio/Strings * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hant/ Translation: TagStudio/Strings * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Luoyu Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hans/ Translation: TagStudio/Strings * Translated using Weblate (Norwegian Bokmål) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Neemek Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nb_NO/ Translation: TagStudio/Strings * Translated using Weblate (Toki Pona) Currently translated at 84.8% (296 of 349 strings) Co-authored-by: Bee Crankson Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/ Translation: TagStudio/Strings * Translated using Weblate (Viossa) Currently translated at 82.8% (289 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/qpv/ Translation: TagStudio/Strings --------- Co-authored-by: searinminecraft Co-authored-by: wany-oh Co-authored-by: ssantos Co-authored-by: Luoyu Co-authored-by: Neemek Co-authored-by: Bee Crankson Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com> --- src/tagstudio/resources/translations/de.json | 12 +- src/tagstudio/resources/translations/es.json | 12 +- src/tagstudio/resources/translations/fil.json | 12 +- src/tagstudio/resources/translations/fr.json | 6 +- src/tagstudio/resources/translations/hu.json | 6 +- src/tagstudio/resources/translations/ja.json | 12 +- .../resources/translations/nb_NO.json | 12 +- src/tagstudio/resources/translations/pl.json | 12 +- src/tagstudio/resources/translations/pt.json | 6 +- .../resources/translations/pt_BR.json | 12 +- src/tagstudio/resources/translations/qpv.json | 12 +- src/tagstudio/resources/translations/ru.json | 12 +- src/tagstudio/resources/translations/ta.json | 12 +- src/tagstudio/resources/translations/tok.json | 12 +- src/tagstudio/resources/translations/tr.json | 12 +- .../resources/translations/zh_Hans.json | 12 +- .../resources/translations/zh_Hant.json | 104 +++++++++--------- 17 files changed, 139 insertions(+), 139 deletions(-) diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 85d47820..2ba80c61 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Führe doppelte Einträge zusammen…", "entries.duplicate.refresh": "Doppelte Einträge aktualisieren", "entries.duplicates.description": "Doppelte Einträge sind definiert als mehrere Einträge, die auf dieselbe Datei auf der Festplatte verweisen. Durch das Zusammenführen dieser Einträge werden die Tags und Metadaten aller Duplikate zu einem einzigen konsolidierten Eintrag zusammengefasst. Diese sind nicht zu verwechseln mit „doppelten Dateien“, die Duplikate Ihrer Dateien selbst außerhalb von TagStudio sind.", + "entries.generic.remove.removing": "Einträge werden gelöscht", "entries.mirror": "Spiegeln", "entries.mirror.confirmation": "Sind Sie sich sicher, dass Sie die folgenden {count} Einträge spiegeln wollen?", "entries.mirror.label": "Spiegele {idx}/{total} Einträge...", "entries.mirror.title": "Einträge werden gespiegelt", "entries.mirror.window_title": "Einträge spiegeln", + "entries.remove.plural.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?", "entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...", "entries.running.dialog.title": "Füge neue Dateieinträge hinzu", "entries.tags": "Tags", - "entries.remove.plural.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?", - "entries.generic.remove.removing": "Einträge werden gelöscht", "entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.

Nicht verknüpfte Einträge können durch das Durchsuchen Ihrer Verzeichnisse automatisch neu verknüpft, vom Benutzer manuell neu verknüpft oder auf Wunsch gelöscht werden.", - "entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}", "entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge wieder zu verknüpfen, {fixed_count} bereits erfolgreich wieder verknüpft", "entries.unlinked.relink.manual": "&Manuell Neuverknüpfen", "entries.unlinked.relink.title": "Einträge werden neuverknüpft", "entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...", "entries.unlinked.search_and_relink": "&Suchen && Neuverbinden", "entries.unlinked.title": "Unverknüpfte Einträge reparieren", + "entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}", "ffmpeg.missing.description": "FFmpeg und/oder FFprobe wurden nicht gefunden. FFmpeg ist für multimediale Wiedergabe und Thumbnails vonnöten.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Feld kopieren", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ Bibliothek

", "json_migration.title.old_lib": "

v9.4 Bibliothek

", "landing.open_create_library": "Bibliothek öffnen/erstellen {shortcut}", - "library_info.stats.entries": "Einträge:", - "library_info.stats.fields": "Felder:", - "library_info.stats.tags": "Tags:", "library.field.add": "Feld hinzufügen", "library.field.confirm_remove": "Wollen Sie dieses \"{name}\" Feld wirklich entfernen?", "library.field.mixed_data": "Gemischte Daten", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "Überprüfe Verzeichnisse auf neue Dateien...\nBereite vor...", "library.refresh.title": "Verzeichnisse werden aktualisiert", "library.scan_library.title": "Bibliothek wird scannen", + "library_info.stats.entries": "Einträge:", + "library_info.stats.fields": "Felder:", + "library_info.stats.tags": "Tags:", "library_object.name": "Name", "library_object.name_required": "Name (erforderlich)", "library_object.slug": "ID Schlüssel", diff --git a/src/tagstudio/resources/translations/es.json b/src/tagstudio/resources/translations/es.json index d51768c3..5974bc17 100644 --- a/src/tagstudio/resources/translations/es.json +++ b/src/tagstudio/resources/translations/es.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Fusionando entradas duplicadas...", "entries.duplicate.refresh": "Recargar entradas duplicadas", "entries.duplicates.description": "Las entradas duplicadas se definen como múltiples entradas que apuntan al mismo archivo en el disco. Al fusionarlas, se combinarán las etiquetas y los metadatos de todos los duplicados en una única entrada consolidada. No deben confundirse con los \"archivos duplicados\", que son duplicados de sus archivos fuera de TagStudio.", + "entries.generic.remove.removing": "Eliminando entradas", "entries.mirror": "&Reflejar", "entries.mirror.confirmation": "¿Estás seguro de que quieres reflejar las siguientes {count} entradas?", "entries.mirror.label": "Reflejando {idx}/{total} Entradas...", "entries.mirror.title": "Reflejando entradas", "entries.mirror.window_title": "Reflejar entradas", + "entries.remove.plural.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?", "entries.running.dialog.new_entries": "Añadiendo {total} nuevas entradas de archivos...", "entries.running.dialog.title": "Añadiendo las nuevas entradas de archivos", "entries.tags": "Etiquetas", - "entries.remove.plural.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?", - "entries.generic.remove.removing": "Eliminando entradas", "entries.unlinked.description": "Cada entrada de la biblioteca está vinculada a un archivo en uno de tus directorios. Si un archivo vinculado a una entrada se mueve o se elimina fuera de TagStudio, se considerará desvinculado.

Las entradas no vinculadas se pueden volver a vincular automáticamente mediante una búsqueda en tus directorios, el usuario puede eliminarlas si así lo desea.", - "entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}", "entries.unlinked.relink.attempting": "Intentando volver a vincular {index}/{unlinked_count} Entradas, {fixed_count} Reenlazado correctamente", "entries.unlinked.relink.manual": "&Reenlace manual", "entries.unlinked.relink.title": "Volver a vincular las entradas", "entries.unlinked.scanning": "Buscando entradas no enlazadas en la biblioteca...", "entries.unlinked.search_and_relink": "&Buscar && volver a vincular", "entries.unlinked.title": "Corregir entradas no vinculadas", + "entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}", "ffmpeg.missing.description": "No se ha encontrado FFmpeg y/o FFprobe. Se requiere de FFmpeg para la reproducción de contenido multimedia y las miniaturas.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Copiar campo", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ biblioteca

", "json_migration.title.old_lib": "

v9.4 biblioteca

", "landing.open_create_library": "Abrir/Crear biblioteca {shortcut}", - "library_info.stats.entries": "Entradas:", - "library_info.stats.fields": "Campos:", - "library_info.stats.tags": "Etiquetas:", "library.field.add": "Añadir campo", "library.field.confirm_remove": "¿Está seguro de que desea eliminar el campo \"{name}\"?", "library.field.mixed_data": "Datos variados", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "Buscar archivos nuevos en los directorios...\nPreparando...", "library.refresh.title": "Refrescar directorios", "library.scan_library.title": "Escaneando la biblioteca", + "library_info.stats.entries": "Entradas:", + "library_info.stats.fields": "Campos:", + "library_info.stats.tags": "Etiquetas:", "library_object.name": "Nombre", "library_object.name_required": "Nombre (Obligatorio)", "library_object.slug": "Slug ID", diff --git a/src/tagstudio/resources/translations/fil.json b/src/tagstudio/resources/translations/fil.json index 65ad70de..68c2df24 100644 --- a/src/tagstudio/resources/translations/fil.json +++ b/src/tagstudio/resources/translations/fil.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Sinasama ang mga Duplicate na Entry…", "entries.duplicate.refresh": "I-refresh ang Mga Duplicate na Entry", "entries.duplicates.description": "Ang mga duplicate na entry ay tinukoy bilang maramihang mga entry na tumuturo sa parehong file sa disk. Ang pagsasama-sama ng mga ito ay pagsasama-samahin ang mga tag at metadata mula sa lahat ng mga duplicate sa isang solong pinagsama-samang entry. Ang mga ito ay hindi dapat ipagkamali sa \"mga duplicate na file\", na mga duplicate ng iyong mga file mismo sa labas ng TagStudio.", + "entries.generic.remove.removing": "Binubura ang Mga Entry", "entries.mirror": "I-&Mirror", "entries.mirror.confirmation": "Sigurado ka ba gusto mong i-mirror ang sumusunod na {count} Mga Entry?", "entries.mirror.label": "Mini-mirror ang {idx}/{total} Mga Entry…", "entries.mirror.title": "Mini-mirror ang Mga Entry", "entries.mirror.window_title": "I-mirror ang Mga Entry", + "entries.remove.plural.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?", "entries.running.dialog.new_entries": "Dinadagdag ang {total} Mga Bagong Entry ng File…", "entries.running.dialog.title": "Dinadagdag ang Mga Bagong Entry ng File", "entries.tags": "Mga Tag", - "entries.remove.plural.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?", - "entries.generic.remove.removing": "Binubura ang Mga Entry", "entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.

Ang mga naka-unlink na entry ay maaring i-link muli sa pamamagitan ng paghahanap sa iyong mga direktoryo o buburahin kung ninanais.", - "entries.unlinked.unlinked_count": "Mga Naka-unlink na Entry: {count}", "entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {index}/{unlinked_count} Mga Entry, {fixed_count} Matagumpay na na-link muli", "entries.unlinked.relink.manual": "&Manwal na Pag-link Muli", "entries.unlinked.relink.title": "Nili-link muli ang Mga Entry", "entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…", "entries.unlinked.search_and_relink": "&Maghanap at Mag-link muli", "entries.unlinked.title": "Ayusin ang Mga Naka-unlink na Entry", + "entries.unlinked.unlinked_count": "Mga Naka-unlink na Entry: {count}", "ffmpeg.missing.description": "Hindi nahanap ang FFmpeg at/o FFprobe. Kinakailangan ang FFmpeg para sa playback ng multimedia at mga thumbnail.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Kopyahin ang Field", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ na Library

", "json_migration.title.old_lib": "

v9.4 na Library

", "landing.open_create_library": "Buksan/Gumawa ng Library {shortcut}", - "library_info.stats.entries": "Mga entry:", - "library_info.stats.fields": "Mga Field:", - "library_info.stats.tags": "Mga tag:", "library.field.add": "Magdagdag ng Field", "library.field.confirm_remove": "Sigurado ka ba gusto mo tanggalin ang field na \"{name}\"?", "library.field.mixed_data": "Halo-halong Data", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "Sina-scan ang Mga Direktoryo para sa Mga Bagong File...\nNaghahanda...", "library.refresh.title": "Nire-refresh ang Mga Direktoryo", "library.scan_library.title": "Sina-scan ang Library", + "library_info.stats.entries": "Mga entry:", + "library_info.stats.fields": "Mga Field:", + "library_info.stats.tags": "Mga tag:", "library_object.name": "Pangalan", "library_object.name_required": "Pangalan (Kinakailangan)", "library_object.slug": "Slug ng ID", diff --git a/src/tagstudio/resources/translations/fr.json b/src/tagstudio/resources/translations/fr.json index f6b1a6ef..66603bca 100644 --- a/src/tagstudio/resources/translations/fr.json +++ b/src/tagstudio/resources/translations/fr.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Fusionner les entrées dupliquées...", "entries.duplicate.refresh": "Rafraichir les Entrées en Doublon", "entries.duplicates.description": "Les entrées dupliquées sont définies comme des entrées multiple qui pointent vers le même fichier sur le disque. Les fusionner va combiner les tags et metadatas de tous les duplicatas vers une seule entrée consolidée. Elles ne doivent pas être confondues avec les \"fichiers en doublon\", qui sont des doublons de vos fichiers en dehors de TagStudio.", + "entries.generic.remove.removing": "Suppression des Entrées", "entries.mirror": "&Refléter", "entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes ?", "entries.mirror.label": "Réplication de {idx}/{total} Entrées...", "entries.mirror.title": "Réplication des Entrées", "entries.mirror.window_title": "Entrée Miroir", + "entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?", "entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...", "entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier", "entries.tags": "Tags", - "entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?", - "entries.generic.remove.removing": "Suppression des Entrées", "entries.unlinked.description": "Chaque entrée dans la bibliothèque est liée à un fichier dans l'un de vos dossiers. Si un fichier lié à une entrée est déplacé ou supprimé en dehors de TagStudio, il est alors considéré non lié.

Les entrées non liées peuvent être automatiquement reliées via la recherche dans vos dossiers, reliées manuellement par l'utilisateur, ou supprimées si désiré.", - "entries.unlinked.unlinked_count": "Entrées non Liées : {count}", "entries.unlinked.relink.attempting": "Tentative de Reliage de {index}/{unlinked_count} Entrées, {fixed_count} ont été Reliées avec Succès", "entries.unlinked.relink.manual": "&Reliage Manuel", "entries.unlinked.relink.title": "Reliage des Entrées", "entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...", "entries.unlinked.search_and_relink": "&Rechercher && Relier", "entries.unlinked.title": "Réparation des Entrées non Liées", + "entries.unlinked.unlinked_count": "Entrées non Liées : {count}", "ffmpeg.missing.description": "FFmpeg et/ou FFprobe n’ont pas été trouvée. FFmpeg est nécessaire pour la lecture de média et les vignettes.", "ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}
{ffprobe} : {ffprobe_status}", "field.copy": "Copier le Champ", diff --git a/src/tagstudio/resources/translations/hu.json b/src/tagstudio/resources/translations/hu.json index 9c504a20..7a57b57c 100644 --- a/src/tagstudio/resources/translations/hu.json +++ b/src/tagstudio/resources/translations/hu.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Egyező elemek egyesítése folyamatban…", "entries.duplicate.refresh": "Egyező elemek &frissítése", "entries.duplicates.description": "Ha több elem ugyanazzal a fájllal van összekapcsolva, akkor egyezőnek számítanak. Ha egyesíti őket, akkor egy olyan elem lesz létrehozva, ami az eredeti elemek összes adatát tartalmazza. Ezeket nem szabad összetéveszteni az „egyező fájlokkal”, amelyek a TagStudión kívüli azonos tartalmú fájlok.", + "entries.generic.remove.removing": "Elemek törlése", "entries.mirror": "&Tükrözés", "entries.mirror.confirmation": "Biztosan tükrözni akarja az alábbi adatokat {count} különböző elemre?", "entries.mirror.label": "{total}/{idx} elem tükrözése folyamatban…", "entries.mirror.title": "Elemek tükrözése", "entries.mirror.window_title": "Elemek tükrözése", + "entries.remove.plural.confirm": "Biztosan törölni akarja az alábbi {count} elemet?", "entries.running.dialog.new_entries": "{total} új elem felvétele folyamatban…", "entries.running.dialog.title": "Új elemek felvétele", "entries.tags": "Címkék", - "entries.remove.plural.confirm": "Biztosan törölni akarja az alábbi {count} elemet?", - "entries.generic.remove.removing": "Elemek törlése", "entries.unlinked.description": "A könyvtár minden eleme egy fájllal van összekapcsolva a számítógépen. Ha egy kapcsolt fájl a TagSudión kívül áthelyezésre vagy törésre kerül, akkor ez a kapcsolat megszakad.

Ezeket a kapcsolat nélküli elemeket a program megpróbálhatja automatikusan megkeresni, de Ön is kézileg újra összekapcsolhatja vagy törölheti őket.", - "entries.unlinked.unlinked_count": "Kapcsolat nélküli elemek: {count}", "entries.unlinked.relink.attempting": "{unlinked_count}/{index} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva", "entries.unlinked.relink.manual": "Új&ra összekapcsolás kézileg", "entries.unlinked.relink.title": "Elemek újra összekapcsolása", "entries.unlinked.scanning": "Kapcsolat nélküli elemek keresése a könyvtárban…", "entries.unlinked.search_and_relink": "&Keresés és újra összekapcsolás", "entries.unlinked.title": "Kapcsolat nélküli elemek javítása", + "entries.unlinked.unlinked_count": "Kapcsolat nélküli elemek: {count}", "ffmpeg.missing.description": "Az FFmpeg és/vagy az FFprobe nem található. Az FFmpeg megléte szükséges a videó- és hangfájlok lejátszásához és a miniatűrök megjelenítéséhez.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Mező &másolása", diff --git a/src/tagstudio/resources/translations/ja.json b/src/tagstudio/resources/translations/ja.json index d16715b7..50339ee1 100644 --- a/src/tagstudio/resources/translations/ja.json +++ b/src/tagstudio/resources/translations/ja.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "重複エントリを統合しています...", "entries.duplicate.refresh": "重複エントリを最新の状態にする", "entries.duplicates.description": "重複エントリとは、ディスク上の同じファイルを指す複数のエントリを指します。これらをマージすると、すべての重複エントリのタグとメタデータが1つのまとまったエントリに統合されます。TagStudio の外部にあるファイル自体の複製である「重複ファイル」と混同しないようにご注意ください。", + "entries.generic.remove.removing": "エントリの削除", "entries.mirror": "ミラー(&M)", "entries.mirror.confirmation": "以下の {count} 件のエントリをミラーリングしてもよろしいですか?", "entries.mirror.label": "{total} 件中 {idx} 件のエントリをミラーリングしています...", "entries.mirror.title": "エントリをミラー", "entries.mirror.window_title": "エントリをミラー", + "entries.remove.plural.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?", "entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...", "entries.running.dialog.title": "新しいファイルエントリを追加", "entries.tags": "タグ", - "entries.remove.plural.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?", - "entries.generic.remove.removing": "エントリの削除", "entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリは未リンクとして扱われます。

未リンクのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。", - "entries.unlinked.unlinked_count": "未リンクのエントリ数: {count}", "entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件の項目を再リンク中、{fixed_count} 件を正常に再リンクしました", "entries.unlinked.relink.manual": "手動で再リンク(&M)", "entries.unlinked.relink.title": "エントリの再リンク", "entries.unlinked.scanning": "未リンクのエントリをライブラリ内でスキャンしています...", "entries.unlinked.search_and_relink": "検索して再リンク(&S)", "entries.unlinked.title": "未リンクのエントリを修正", + "entries.unlinked.unlinked_count": "未リンクのエントリ数: {count}", "ffmpeg.missing.description": "FFmpeg または FFprobe が見つかりません。マルチメディアの再生とサムネイルの表示には FFmpeg のインストールが必要です。", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "フィールドをコピー", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ ライブラリ

", "json_migration.title.old_lib": "

v9.4 ライブラリ

", "landing.open_create_library": "ライブラリを開く/作成する {shortcut}", - "library_info.stats.entries": "エントリ:", - "library_info.stats.fields": "フィールド:", - "library_info.stats.tags": "タグ:", "library.field.add": "フィールドの追加", "library.field.confirm_remove": "「{name}」フィールドを削除してもよろしいですか?", "library.field.mixed_data": "混在データ", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "新しいファイルを検索中...\n準備中...", "library.refresh.title": "ディレクトリを更新しています", "library.scan_library.title": "ライブラリをスキャンしています", + "library_info.stats.entries": "エントリ:", + "library_info.stats.fields": "フィールド:", + "library_info.stats.tags": "タグ:", "library_object.name": "名前", "library_object.name_required": "名前 (必須)", "library_object.slug": "ID スラッグ", diff --git a/src/tagstudio/resources/translations/nb_NO.json b/src/tagstudio/resources/translations/nb_NO.json index c8e5591d..55390d45 100644 --- a/src/tagstudio/resources/translations/nb_NO.json +++ b/src/tagstudio/resources/translations/nb_NO.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Fletter duplikatoppføringer…", "entries.duplicate.refresh": "Oppdater Duplikate Oppføringer", "entries.duplicates.description": "Duplikate oppføringer er definert som flere oppføringer som peker til samme fil på disken. Å slå disse sammen vil kombinere etikettene og metadataen fra alle duplikater til én enkel oppføring. Disse må ikke forveksles med \"duplikate filer\", som er duplikater av selve filene utenfor TagStudio.", + "entries.generic.remove.removing": "Sletter Oppføringer", "entries.mirror": "Speil", "entries.mirror.confirmation": "Er du sikker på at du vil speile følgende {count} Oppføringer?", "entries.mirror.label": "Speiler {idx}/{total} Oppføringer...", "entries.mirror.title": "Speiler Oppføringer", "entries.mirror.window_title": "Speil Oppføringer", + "entries.remove.plural.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?", "entries.running.dialog.new_entries": "Legger til {total} Nye Filoppføringer...", "entries.running.dialog.title": "Legger til Nye Filoppføringer", "entries.tags": "Etiketter", - "entries.remove.plural.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?", - "entries.generic.remove.removing": "Sletter Oppføringer", "entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.

Frakoblede oppføringer kan bli automatisk gjenkoblet ved å søke i mappene dine eller slettet om det er ønsket.", - "entries.unlinked.unlinked_count": "Frakoblede Oppføringer: {count}", "entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {index}/{unlinked_count} Oppføringer, {fixed_count} Klart Gjenkoblet", "entries.unlinked.relink.manual": "&Manuell Gjenkobling", "entries.unlinked.relink.title": "Gjenkobler Oppføringer", "entries.unlinked.scanning": "Skanner bibliotek for ulenkede oppføringer …", "entries.unlinked.search_and_relink": "&Søk && Gjenkobl", "entries.unlinked.title": "Fiks ulenkede oppføringer", + "entries.unlinked.unlinked_count": "Frakoblede Oppføringer: {count}", "ffmpeg.missing.description": "FFmpeg og/eller FFprobe ble ikke funnet. FFmpeg er påkrevd for flermediell gjenspilling og miniatyrbilde.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Kopier Felt", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ Bibliotek

", "json_migration.title.old_lib": "

v9.4 Bibliotek

", "landing.open_create_library": "Åpne/Lag nytt Bibliotek {shortcut}", - "library_info.stats.entries": "Oppføringer:", - "library_info.stats.fields": "Felter:", - "library_info.stats.tags": "Etiketter:", "library.field.add": "Legg til felt", "library.field.confirm_remove": "Fjern dette «\"{name}\"»-feltet?", "library.field.mixed_data": "Blandet data", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "Skanner Mapper for Nye Filer...\nForbereder...", "library.refresh.title": "Oppdaterer Mapper", "library.scan_library.title": "Skanning av bibliotek", + "library_info.stats.entries": "Oppføringer:", + "library_info.stats.fields": "Felter:", + "library_info.stats.tags": "Etiketter:", "library_object.name": "Navn", "library_object.name_required": "Navn (Påkrevd)", "library_object.slug": "ID Slug", diff --git a/src/tagstudio/resources/translations/pl.json b/src/tagstudio/resources/translations/pl.json index 59b802b4..c83c72ea 100644 --- a/src/tagstudio/resources/translations/pl.json +++ b/src/tagstudio/resources/translations/pl.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Łączenie zduplikowanych wpisów...", "entries.duplicate.refresh": "Odśwież zduplikowane wpisy", "entries.duplicates.description": "Zduplikowane wpisy są zdefiniowane jako wielokrotne wpisy które wskazują na ten sam plik na dysku. Złączenie ich złączy tagi i metadane ze wszystkich duplikatów w jeden wpis. Nie mylić tego ze \"zduplikowanymi plikami\" które są duplikatami samych plików poza TagStudio.", + "entries.generic.remove.removing": "Usuwanie wpisów", "entries.mirror": "&Odzwierciedl", "entries.mirror.confirmation": "Jesteś pewien że chcesz odzwierciedlić następujące {count} wpisy?", "entries.mirror.label": "Odzwierciedlanie {idx}/{total} wpisów...", "entries.mirror.title": "Odzwierciedlanie wpisów", "entries.mirror.window_title": "Odzwierciedl wpisy", + "entries.remove.plural.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?", "entries.running.dialog.new_entries": "Dodawanie {total} nowych wpisów plików...", "entries.running.dialog.title": "Dodawanie nowych wpisów plików", "entries.tags": "Tagi", - "entries.remove.plural.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?", - "entries.generic.remove.removing": "Usuwanie wpisów", "entries.unlinked.description": "Każdy wpis w bibliotece jest połączony z plikiem w jednym z twoich katalogów. Jeśli połączony plik jest przeniesiony poza TagStudio albo usunięty to jest uważany za odłączony.

Odłączone wpisy mogą być automatycznie połączone ponownie przez szukanie twoich katalogów, ręczne ponowne łączenie przez użytkownika lub usunięte jeśli zajdzie taka potrzeba.", - "entries.unlinked.unlinked_count": "Odłączone wpisy: {count}", "entries.unlinked.relink.attempting": "Próbowanie ponownego łączenia {index}/{unlinked_count} wpisów, {fixed_count} poprawnie połączono ponownie", "entries.unlinked.relink.manual": "&Ręczne ponowne łączenie", "entries.unlinked.relink.title": "Ponowne łączenie wpisów", "entries.unlinked.scanning": "Skanowanie biblioteki dla odłączonych wpisów...", "entries.unlinked.search_and_relink": "&Wyszukaj && Zalinkuj ponownie", "entries.unlinked.title": "Napraw odłączone wpisy", + "entries.unlinked.unlinked_count": "Odłączone wpisy: {count}", "ffmpeg.missing.description": "Nie odnaleziono FFmpeg lub FFprobe. FFmpeg jest wymagany do odtwarzania multimediów i do wyświetlania miniaturek.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Skopiuj pole", @@ -154,9 +154,6 @@ "json_migration.title.new_lib": "

Biblioteka v9.5+

", "json_migration.title.old_lib": "

Biblioteka v9.4

", "landing.open_create_library": "Otwórz/Stwórz bibliotekę {shortcut}", - "library_info.stats.entries": "Wpisy:", - "library_info.stats.fields": "Pola:", - "library_info.stats.tags": "Tagi:", "library.field.add": "Dodaj pole", "library.field.confirm_remove": "Jesteś pewien że chcesz usunąć pole \"{name}\" ?", "library.field.mixed_data": "Mieszane dane", @@ -167,6 +164,9 @@ "library.refresh.scanning_preparing": "Skanowanie katalogów w poszukiwaniu nowych plików\nPrzygotowywanie...", "library.refresh.title": "Odświeżanie katalogów", "library.scan_library.title": "Skanowanie biblioteki", + "library_info.stats.entries": "Wpisy:", + "library_info.stats.fields": "Pola:", + "library_info.stats.tags": "Tagi:", "library_object.name": "Nazwa", "library_object.name_required": "Nazwa (wymagana)", "macros.running.dialog.new_entries": "Stosowanie skonfigurowanych makr na {count}/{total} nowych wpisach plików...", diff --git a/src/tagstudio/resources/translations/pt.json b/src/tagstudio/resources/translations/pt.json index 80249285..cbd27f3a 100644 --- a/src/tagstudio/resources/translations/pt.json +++ b/src/tagstudio/resources/translations/pt.json @@ -39,24 +39,24 @@ "entries.duplicate.merge.label": "A Mesclar Elementos Duplicados...", "entries.duplicate.refresh": "Atualizar Registos Duplicados", "entries.duplicates.description": "Registos duplicados são definidas como múltiplos registos que levam ao mesmo ficheiro no disco. Mesclar estes registos irá combinar as tags e metadados de todas as duplicatas num único registo consolidado. Não confundir com \"Ficheiros Duplicados\" que são duplicatas dos seus ficheiros fora do TagStudio.", + "entries.generic.remove.removing": "A Apagar Registos", "entries.mirror": "&Espelho", "entries.mirror.confirmation": "Tem certeza que deseja espelhar os seguintes {count} registos?", "entries.mirror.label": "A Espelhar {idx}/{total} Registos...", "entries.mirror.title": "A Espelhar Registos", "entries.mirror.window_title": "Espelhar Registos", + "entries.remove.plural.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?", "entries.running.dialog.new_entries": "A Adicionar {total} Novos Registos de Ficheiros...", "entries.running.dialog.title": "A Adicionar Novos Registos de Ficheiros", "entries.tags": "Tags", - "entries.remove.plural.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?", - "entries.generic.remove.removing": "A Apagar Registos", "entries.unlinked.description": "Cada registo na biblioteca faz referência à um ficheiro numa das suas pastas. Se um ficheiro referenciado à uma entrada for movido ou apagado fora do TagStudio, ele é depois considerado não-referenciado.

Registos não-referenciados podem ser automaticamente referenciados por pesquisas nos seus diretórios, manualmente pelo utilizador, ou apagado se for desejado.", - "entries.unlinked.unlinked_count": "Registos Não Referenciados: {count}", "entries.unlinked.relink.attempting": "A tentar referenciar {index}/{unlinked_count} Registos, {fixed_count} Referenciados com Sucesso", "entries.unlinked.relink.manual": "&Referência Manual", "entries.unlinked.relink.title": "A Referenciar Registos", "entries.unlinked.scanning": "A escanear biblioteca por registos não referenciados...", "entries.unlinked.search_and_relink": "&Pesquisar && Referenciar", "entries.unlinked.title": "Corrigir Registos Não Referenciados", + "entries.unlinked.unlinked_count": "Registos Não Referenciados: {count}", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Copiar Campo", "field.edit": "Editar Campo", diff --git a/src/tagstudio/resources/translations/pt_BR.json b/src/tagstudio/resources/translations/pt_BR.json index 71845892..41ed96df 100644 --- a/src/tagstudio/resources/translations/pt_BR.json +++ b/src/tagstudio/resources/translations/pt_BR.json @@ -37,24 +37,24 @@ "entries.duplicate.merge.label": "Mesclando Itens Duplicados...", "entries.duplicate.refresh": "Atualizar Registros Duplicados", "entries.duplicates.description": "Registros duplicados são definidas como multiplos registros que levam ao mesmo arquivo no disco. Mesclar esses registros irá combinar as tags e metadados de todas as duplicatas em um único registro consolidado. Não confundir com \"Arquivos Duplicados\" que são duplicatas dos seus arquivos fora do TagStudio.", + "entries.generic.remove.removing": "Deletando Registros", "entries.mirror": "&Espelho", "entries.mirror.confirmation": "Tem certeza que você deseja espelhar os seguintes {count} registros?", "entries.mirror.label": "Espelhando {idx}/{total} Registros...", "entries.mirror.title": "Espelhando Registros", "entries.mirror.window_title": "Espelhar Registros", + "entries.remove.plural.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?", "entries.running.dialog.new_entries": "Adicionando {total} Novos Registros de Arquivos...", "entries.running.dialog.title": "Adicionando Novos Registros de Arquivos", "entries.tags": "Tags", - "entries.remove.plural.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?", - "entries.generic.remove.removing": "Deletando Registros", "entries.unlinked.description": "Cada registro na biblioteca faz referência à um arquivo em uma de suas pastas. Se um arquivo referenciado à uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não-referenciado.

Registros não-referenciados podem ser automaticamente referenciados por buscas nos seus diretórios, manualmente pelo usuário, ou deletado se for desejado.", - "entries.unlinked.unlinked_count": "Registros Não Referenciados: {count}", "entries.unlinked.relink.attempting": "Tentando referenciar {index}/{unlinked_count} Registros, {fixed_count} Referenciados com Sucesso", "entries.unlinked.relink.manual": "&Referência Manual", "entries.unlinked.relink.title": "Referenciando Registros", "entries.unlinked.scanning": "Escaneando bibliotecada em busca de registros não referenciados...", "entries.unlinked.search_and_relink": "&Buscar && Referenciar", "entries.unlinked.title": "Corrigir Registros Não Referenciados", + "entries.unlinked.unlinked_count": "Registros Não Referenciados: {count}", "field.copy": "Copiar Campo", "field.edit": "Editar Campo", "field.paste": "Colar Campo", @@ -141,9 +141,6 @@ "json_migration.title.new_lib": "

Biblioteca v9.5+

", "json_migration.title.old_lib": "

Biblioteca v9.4

", "landing.open_create_library": "Abrir/Criar Biblioteca {shortcut}", - "library_info.stats.entries": "Registros:", - "library_info.stats.fields": "Campos:", - "library_info.stats.tags": "Tags:", "library.field.add": "Adicionar Campo", "library.field.confirm_remove": "Você tem certeza de que quer remover o campo \"{name}\"?", "library.field.mixed_data": "Dados Mistos", @@ -155,6 +152,9 @@ "library.refresh.scanning_preparing": "Escaneando Diretórios por Novos Arquivos...\nPreparando...", "library.refresh.title": "Atualizando Pastas", "library.scan_library.title": "Escaneando Biblioteca", + "library_info.stats.entries": "Registros:", + "library_info.stats.fields": "Campos:", + "library_info.stats.tags": "Tags:", "library_object.name": "Nome", "library_object.name_required": "Nome (Obrigatório)", "macros.running.dialog.new_entries": "Executando Macros Configurados nos {count}/{total} Novos Registros de Arquivos...", diff --git a/src/tagstudio/resources/translations/qpv.json b/src/tagstudio/resources/translations/qpv.json index 07b54b31..5b68f148 100644 --- a/src/tagstudio/resources/translations/qpv.json +++ b/src/tagstudio/resources/translations/qpv.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Visk sama shiruzmakaban ima...", "entries.duplicate.refresh": "Gotova sama shiruzmakaban gen", "entries.duplicates.description": "Mverm fu shiruzmakaban implajena na plus ka ein shiruzmakaban ke tsunaga na sama mlafu na shiruzmabaksu. Na visk afto, zol festa festaretol au mlafushiruzma al mverm fu mlafu kara ine ein stuur shiruzmakaban. Hej nai sama \"mverm fu mlafu\", ke mverm fu mlafu fu du, ekso TagStudio.", + "entries.generic.remove.removing": "Keste shiruzmakaban ima", "entries.mirror": "&Maha melon fu", "entries.mirror.confirmation": "Du mahatsa melon fu afto {count} shiruzmakaban?", "entries.mirror.label": "Maha melon fu {idx}/{total} shiruzmakaban ima...", "entries.mirror.title": "Maha melon fu shiruzmakaban ima", "entries.mirror.window_title": "Maha melon fu shiruzmakaban", + "entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?", "entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...", "entries.running.dialog.title": "Nasii neo shiruzmakaban fu mlafu ima", "entries.tags": "Festaretol", - "entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?", - "entries.generic.remove.removing": "Keste shiruzmakaban ima", "entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore tsunaganaijena.

Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.", - "entries.unlinked.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}", "entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen", "entries.unlinked.relink.manual": "&Tsunaga gen mit hant", "entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen", "entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...", "entries.unlinked.search_and_relink": "&Suha &&Tsunaga gen", "entries.unlinked.title": "Fiks tsunaganaijena shiruzmakaban", + "entries.unlinked.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}", "ffmpeg.missing.description": "FFmpeg au/os FFprobe nai finnajena. TagStudio treng FFmpeg per mahase riso.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Mverm shiruzmafal", @@ -152,9 +152,6 @@ "json_migration.title.new_lib": "

v9.5+ Mlafuhuomi

", "json_migration.title.old_lib": "

v9.4 Mlafuhuomi

", "landing.open_create_library": "Auki/Maha mlafuhuomi {shortcut}", - "library_info.stats.entries": "Shiruzmakaban:", - "library_info.stats.fields": "Shiruzmafal:", - "library_info.stats.tags": "Festaretol:", "library.field.add": "Nasii shiruzmafal", "library.field.confirm_remove": "Du kestetsa afto \"{name}\" shiruzmafal we?", "library.field.mixed_data": "Viskena shiruzma", @@ -166,6 +163,9 @@ "library.refresh.scanning_preparing": "Taskama mlafukaban fu neo mlafu ima...\nGotova ima...", "library.refresh.title": "Gengotova al mlafukaban", "library.scan_library.title": "Taskama mlafuhuomi ima", + "library_info.stats.entries": "Shiruzmakaban:", + "library_info.stats.fields": "Shiruzmafal:", + "library_info.stats.tags": "Festaretol:", "library_object.name": "Namae", "library_object.name_required": "Namae (Trengjena)", "library_object.slug": "ID Slug", diff --git a/src/tagstudio/resources/translations/ru.json b/src/tagstudio/resources/translations/ru.json index 42985657..8c7e9154 100644 --- a/src/tagstudio/resources/translations/ru.json +++ b/src/tagstudio/resources/translations/ru.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "Объединение записей-дубликатов...", "entries.duplicate.refresh": "Обновить записи-дубликаты", "entries.duplicates.description": "Записи-дубликаты — это несколько записей, которые одновременно привязаны к одному файлу. Объединение таких дубликатов соединит все теги и мета данные из этих записей в одну. Записи-дубликаты не стоит путать с несколькими копиями самого файла, которые могут существовать вне TagStudio.", + "entries.generic.remove.removing": "Удаление записей", "entries.mirror": "&Отзеркалить", "entries.mirror.confirmation": "Вы уверены, что хотите отзеркалить следующие {count} записей?", "entries.mirror.label": "Отзеркаливание {idx}/{total} записей...", "entries.mirror.title": "Отзеркаливание записей", "entries.mirror.window_title": "Отзеркалить записи", + "entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?", "entries.running.dialog.new_entries": "Добавление {total} новых записей...", "entries.running.dialog.title": "Добавление новых записей", "entries.tags": "Теги", - "entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?", - "entries.generic.remove.removing": "Удаление записей", "entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".

Откреплённые записи могут быть прикреплены обратно автоматически, либо же удалены если в них нет надобности.", - "entries.unlinked.unlinked_count": "Откреплённых записей: {count}", "entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно", "entries.unlinked.relink.manual": "&Ручная привязка", "entries.unlinked.relink.title": "Привязка записей", "entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...", "entries.unlinked.search_and_relink": "&Поиск и привязка", "entries.unlinked.title": "Исправить откреплённые записи", + "entries.unlinked.unlinked_count": "Откреплённых записей: {count}", "ffmpeg.missing.description": "FFmpeg и/или FFprobe не были найдены. FFmpeg необходим для воспроизведения мультимедиа и превью.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "Копировать поле", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

Библиотека версии 9.5+

", "json_migration.title.old_lib": "

Библиотека версии 9.4

", "landing.open_create_library": "Открыть/создать библиотеку {shortcut}", - "library_info.stats.entries": "Записи:", - "library_info.stats.fields": "Поля:", - "library_info.stats.tags": "Теги:", "library.field.add": "Добавить поле", "library.field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?", "library.field.mixed_data": "Смешанные данные", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "Сканирование папок на наличие новых файлов...\nПодготовка...", "library.refresh.title": "Обновление папок", "library.scan_library.title": "Сканирование библиотеки", + "library_info.stats.entries": "Записи:", + "library_info.stats.fields": "Поля:", + "library_info.stats.tags": "Теги:", "library_object.name": "Название", "library_object.name_required": "Название (Обязательно)", "library_object.slug": "Уникальный ID", diff --git a/src/tagstudio/resources/translations/ta.json b/src/tagstudio/resources/translations/ta.json index 75ac46b3..6b57600b 100644 --- a/src/tagstudio/resources/translations/ta.json +++ b/src/tagstudio/resources/translations/ta.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "நகல் உள்ளீடுகளை ஒன்றிணைத்தல் ...", "entries.duplicate.refresh": "நகல் உள்ளீடுகளைப் புதுப்பி", "entries.duplicates.description": "மறுநுழைவுகள் என்பது, ஒரே கோப்பை குறிக்கும் பல நுழைவுகளை குறிக்கும். இவற்றை இணைப்பதால், அனைத்து மறுநுழைவுகளின் குறிச்சொற்களும் மெட்டாடேட்டாவும் ஒரே ஒட்டுமொத்த நுழைவாகச் சேர்க்கப்படும். இவற்றை 'மறுகோப்புகள்' என்பதுடன் குழப்பக் கூடாது, ஏனெனில் அவை டாக் ஸ்டுடியோவுக்கு வெளியேயுள்ள கோப்புகளின் நகல்களாகும்.", + "entries.generic.remove.removing": "உள்ளீடுகள் நீக்கப்படுகிறது", "entries.mirror": "& கண்ணாடி", "entries.mirror.confirmation": "பின்வரும் உள்ளீடுகளைப் பிரதிபலிக்க விரும்புகிறீர்களா {count}?", "entries.mirror.label": "{idx}/{total} உள்ளீடுகளைப் பிரதிபலிக்கப்படுகின்றது...", "entries.mirror.title": "உள்ளீடுகள் பிரதிபழிக்கப்படுகின்றது", "entries.mirror.window_title": "கண்ணாடி உள்ளீடுகள்", + "entries.remove.plural.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?", "entries.running.dialog.new_entries": "{total} புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது ...", "entries.running.dialog.title": "புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது", "entries.tags": "குறிச்சொற்கள்", - "entries.remove.plural.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?", - "entries.generic.remove.removing": "உள்ளீடுகள் நீக்கப்படுகிறது", "entries.unlinked.description": "ஒவ்வொரு நூலக நுழைவும் உங்கள் கோப்பகங்களில் ஒன்றில் ஒரு கோப்போடு இணைக்கப்பட்டுள்ளது. ஒரு நுழைவுடன் இணைக்கப்பட்ட ஒரு கோப்பு டாக்ச்டுடியோவுக்கு வெளியே நகர்த்தப்பட்டால் அல்லது நீக்கப்பட்டால், அது பின்னர் இணைக்கப்படாததாகக் கருதப்படுகிறது.", - "entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}", "entries.unlinked.relink.attempting": "{index}/{unlinked_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது", "entries.unlinked.relink.manual": "& கையேடு மறுபரிசீலனை", "entries.unlinked.relink.title": "உள்ளீடுகள் மீண்டும் இணைக்கப்படுகின்றது", "entries.unlinked.scanning": "இணைக்கப்படாத நுழைவுகளை புத்தககல்லரியில் சோதனை செய்யப்படுகிறது...", "entries.unlinked.search_and_relink": "& தேடல் && relink", "entries.unlinked.title": "இணைக்கப்படாத உள்ளீடுகளைச் சரிசெய்யவும்", + "entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}", "ffmpeg.missing.description": "FFMPEG மற்றும்/அல்லது FFPROBE கண்டுபிடிக்கப்படவில்லை. மல்டிமீடியா பிளேபேக் மற்றும் சிறுபடங்களுக்கு FFMPEG தேவைப்படுகிறது.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "நகல் புலம்", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

V9.5+ நூலகம்

", "json_migration.title.old_lib": "

V9.4 நூலகம்

", "landing.open_create_library": "நூலகத்தைத் திறக்கவும்/உருவாக்கவும் {shortcut}", - "library_info.stats.entries": "உள்ளீடுகள்:", - "library_info.stats.fields": "புலங்கள்:", - "library_info.stats.tags": "குறிச்சொற்கள்:", "library.field.add": "புலத்தைச் சேர்க்க", "library.field.confirm_remove": "இந்த \"{name}\" புலத்தை நிச்சயமாக அகற்ற விரும்புகிறீர்களா?", "library.field.mixed_data": "கலப்பு தரவு", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "புதிய கோப்புகளுக்கான அடைவுகள் சோதனை செய்யப்படுகின்றது...\nதயாராகிறது...", "library.refresh.title": "கோப்பகங்கள் புதுப்பிக்கப்படுகின்றன", "library.scan_library.title": "புத்தககல்லரி சோதனை செய்யப்படுகிறது", + "library_info.stats.entries": "உள்ளீடுகள்:", + "library_info.stats.fields": "புலங்கள்:", + "library_info.stats.tags": "குறிச்சொற்கள்:", "library_object.name": "பெயர்", "library_object.name_required": "பெயர் (தேவை)", "library_object.slug": "ஐடி ச்லக்", diff --git a/src/tagstudio/resources/translations/tok.json b/src/tagstudio/resources/translations/tok.json index c9657c60..c54f0b8f 100644 --- a/src/tagstudio/resources/translations/tok.json +++ b/src/tagstudio/resources/translations/tok.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "mi wan e ijo sama...", "entries.duplicate.refresh": "o kama jo e sona tan ijo sama", "entries.duplicates.description": "ken la, ijo mute li jo e ijo lon sama. ni li \"ijo sama\". sina wan e ona la, ijo sama li kama wan li jo e sona ale tan ijo sama ale.", + "entries.generic.remove.removing": "mi weka e ijo", "entries.mirror": "jasi&ma", "entries.mirror.confirmation": "mi jasima e ijo {count}. ni li pona anu seme?", "entries.mirror.label": "mi jasima e ijo {idx}/{total}...", "entries.mirror.title": "mi jasima e ijo", "entries.mirror.window_title": "o jasima e ijo", + "entries.remove.plural.confirm": "mi weka e ijo {count}. ni li pona anu seme?", "entries.running.dialog.new_entries": "mi pana e lipu sin {total}...", "entries.running.dialog.title": "mi pana e lipu sin", "entries.tags": "poki", - "entries.remove.plural.confirm": "mi weka e ijo {count}. ni li pona anu seme?", - "entries.generic.remove.removing": "mi weka e ijo", "entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.

ijo pi ijo lon li ken alasa lon tomo li ken kama jo e ijo lon. ante la sina ken weka e ona.", - "entries.unlinked.unlinked_count": "ijo pi ijo lon ala: {count}", "entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {index}/{unlinked_count}. mi pana e ijo lon tawa ijo {fixed_count}", "entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo (&M)", "entries.unlinked.relink.title": "mi pana e ijo lon tawa ijo", "entries.unlinked.scanning": "mi o alasa e ijo pi ijo lon ala...", "entries.unlinked.search_and_relink": "o ala&sa o pana e ijo lon tawa ijo", "entries.unlinked.title": "o pona e ijo pi ijo lon ala", + "entries.unlinked.unlinked_count": "ijo pi ijo lon ala: {count}", "ffmpeg.missing.description": "mi lukin ala e ilo FFmpeg e/anu ilo FFprobe. sina wile e kepeken sin pi musi mute e sitelen lili pi musi mute la sina wile e ilo FFmpeg.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "o kama jo e ma sama", @@ -150,9 +150,6 @@ "json_migration.title.new_lib": "

tomo pi ilo nanpa 9.5+

", "json_migration.title.old_lib": "

tomo pi ilo nanpa 9.4

", "landing.open_create_library": "o open anu pali sin e tomo {shortcut}", - "library_info.stats.entries": "ijo:", - "library_info.stats.fields": "ma:", - "library_info.stats.tags": "poki:", "library.field.add": "o pana e ma", "library.field.confirm_remove": "sina wile ala wile weka e ma \"{name}\" ni?", "library.field.mixed_data": "sona nasa", @@ -164,6 +161,9 @@ "library.refresh.scanning_preparing": "mi alasa e ijo sin lon tomo...\nmi kama pona...", "library.refresh.title": "mi kama jo e sin lon tomo", "library.scan_library.title": "mi o lukin e tomo", + "library_info.stats.entries": "ijo:", + "library_info.stats.fields": "ma:", + "library_info.stats.tags": "poki:", "library_object.name": "nimi", "library_object.name_required": "nimi (wile mute)", "library_object.slug": "ID Slug", diff --git a/src/tagstudio/resources/translations/tr.json b/src/tagstudio/resources/translations/tr.json index bb83d6da..14f1834f 100644 --- a/src/tagstudio/resources/translations/tr.json +++ b/src/tagstudio/resources/translations/tr.json @@ -39,24 +39,24 @@ "entries.duplicate.merge.label": "Yinelenen Kayıtlar Birleştiriliyor...", "entries.duplicate.refresh": "Yinelenen Kayıtları Yenile", "entries.duplicates.description": "Yinelenen kayıtlar, diskinizde aynı dosyaya işaret eden birden fazla kayıt olarak tanımlanmaktadır. Bu kayıtları birleştirdiğinizde, yinelenen tüm kayıtların içerisindeki etiketler ve metadata bilgisi tek bir tane kayıt üzerinde birleştirilecektir. Bu, \"yinelenen dosyalar\" ile karıştırılmamalıdır. Yinelenen dosyalar, TagStudio'nun dışında birden fazla kere bulunan dosyalarınızdır.", + "entries.generic.remove.removing": "Kayıtlar Siliniyor", "entries.mirror": "&Yansıt", "entries.mirror.confirmation": "{count} kaydı yansıtmak istediğinden emin misin?", "entries.mirror.label": "{idx}/{total} Kayıt Yansıtılıyor...", "entries.mirror.title": "Kayıtlar Yansıtılıyor", "entries.mirror.window_title": "Kayıtları Yansıt", + "entries.remove.plural.confirm": "{count} tane kayıtları silmek istediğinden emin misin?", "entries.running.dialog.new_entries": "{total} Yeni Dosya Kaydı Ekleniyor...", "entries.running.dialog.title": "Yeni Dosya Kayıtları Ekleniyor", "entries.tags": "Etiketler", - "entries.remove.plural.confirm": "{count} tane kayıtları silmek istediğinden emin misin?", - "entries.generic.remove.removing": "Kayıtlar Siliniyor", "entries.unlinked.description": "Kütüphanenizdeki her bir kayıt, dizinlerinizden bir tane dosya ile eşleştirilmektedir. Eğer bir kayıta bağlı dosya TagStudio dışında taşınır veya silinirse, o dosya artık kopmuş olarak sayılır.

Kopmuş kayıtlar dizinlerinizde arama yapılırken otomatik olarak tekrar eşleştirilebilir, manuel olarak sizin tarafınızdan eşleştirilebilir veya isteğiniz üzere silinebilir.", - "entries.unlinked.unlinked_count": "Kopmuş Kayıtlar: {count}", "entries.unlinked.relink.attempting": "{index}/{unlinked_count} Kayıt Yeniden Eşleştirilmeye Çalışılıyor, {fixed_count} Başarıyla Yeniden Eşleştirildi", "entries.unlinked.relink.manual": "&Manuel Yeniden Eşleştirme", "entries.unlinked.relink.title": "Kayıtlar Yeniden Eşleştiriliyor", "entries.unlinked.scanning": "Kütüphane, Kopmuş Kayıtlar için Taranıyor...", "entries.unlinked.search_and_relink": "&Ara && Yeniden Eşleştir", "entries.unlinked.title": "Kopmuş Kayıtları Düzelt", + "entries.unlinked.unlinked_count": "Kopmuş Kayıtlar: {count}", "field.copy": "Ek Bilgiyi Kopyala", "field.edit": "Ek Bilgiyi Düzenle", "field.paste": "Ek Bilgiyi Yapıştır", @@ -152,9 +152,6 @@ "json_migration.title.new_lib": "

v9.5+ Kütüphane

", "json_migration.title.old_lib": "

v9.4 Kütüphane

", "landing.open_create_library": "Kütüphane Aç/Oluştur {shortcut}", - "library_info.stats.entries": "Kayıtlar:", - "library_info.stats.fields": "Ek Bilgiler:", - "library_info.stats.tags": "Etiketler:", "library.field.add": "Ek Bilgi Ekle", "library.field.confirm_remove": "Bu \"{name}\" ek bilgisini silmek istediğinden emin misin?", "library.field.mixed_data": "Karışık Veri", @@ -166,6 +163,9 @@ "library.refresh.scanning_preparing": "Yeni Dosyalar için Dizinler Taranıyor...\nHazırlanıyor...", "library.refresh.title": "Dizinler Yenileniyor", "library.scan_library.title": "Kütüphane Taranıyor", + "library_info.stats.entries": "Kayıtlar:", + "library_info.stats.fields": "Ek Bilgiler:", + "library_info.stats.tags": "Etiketler:", "library_object.name": "Ad", "library_object.name_required": "Ad (Gerekli)", "library_object.slug": "Kimlik Kodu", diff --git a/src/tagstudio/resources/translations/zh_Hans.json b/src/tagstudio/resources/translations/zh_Hans.json index e398b694..35faf552 100644 --- a/src/tagstudio/resources/translations/zh_Hans.json +++ b/src/tagstudio/resources/translations/zh_Hans.json @@ -40,24 +40,24 @@ "entries.duplicate.merge.label": "正在合并重复项目...", "entries.duplicate.refresh": "重新整理重复项目", "entries.duplicates.description": "重复项目被定义为多个指向磁盘上同一文件的项目。合并这些项目将把所有重复项目的标签和元数据整合为一个统一的项目。这与“重复文件”不同,后者是指在 TagStudio 之外的文件本身的重复。", + "entries.generic.remove.removing": "正在删除项目", "entries.mirror": "镜像(&m)", "entries.mirror.confirmation": "您确定要镜像以下 {count} 条项目吗?", "entries.mirror.label": "正在镜像 {idx}/{total} 个项目...", "entries.mirror.title": "进行项目镜像", "entries.mirror.window_title": "项目镜像", + "entries.remove.plural.confirm": "您确定要删除以下 {count} 个项目?", "entries.running.dialog.new_entries": "正在加入 {total} 个新文件项目...", "entries.running.dialog.title": "正在加入新文件项目", "entries.tags": "标签", - "entries.remove.plural.confirm": "您确定要删除以下 {count} 个项目?", - "entries.generic.remove.removing": "正在删除项目", "entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。

未链接的条目可能会通过搜索目录自动重新链接,或者根据需要删除。", - "entries.unlinked.unlinked_count": "未链接的项目: {count}", "entries.unlinked.relink.attempting": "正在尝试重新链接 {index}/{unlinked_count} 个项目, {fixed_count} 个项目成功重链", "entries.unlinked.relink.manual": "手动重新链接(&m)", "entries.unlinked.relink.title": "正在重新链接项目", "entries.unlinked.scanning": "正在扫描仓库以寻找未链接的项目...", "entries.unlinked.search_and_relink": "搜索并重新链接(&s)", "entries.unlinked.title": "修复未链接的项目", + "entries.unlinked.unlinked_count": "未链接的项目: {count}", "ffmpeg.missing.description": "找不到 FFmpeg 或 FFprobe。多媒体播放和缩略图生成需要 FFmpeg 支持。", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "复制字段", @@ -156,9 +156,6 @@ "json_migration.title.new_lib": "

v9.5+ 仓库

", "json_migration.title.old_lib": "

v9.4 仓库

", "landing.open_create_library": "打开/创建仓库 {shortcut}", - "library_info.stats.entries": "项目:", - "library_info.stats.fields": "字段:", - "library_info.stats.tags": "标签:", "library.field.add": "新增字段", "library.field.confirm_remove": "您确定要移除此 \"{name}\" 字段?", "library.field.mixed_data": "混合数据", @@ -170,6 +167,9 @@ "library.refresh.scanning_preparing": "正在扫描文件夹中的新文件...\n准备中...", "library.refresh.title": "正在刷新目录", "library.scan_library.title": "正在扫描仓库", + "library_info.stats.entries": "项目:", + "library_info.stats.fields": "字段:", + "library_info.stats.tags": "标签:", "library_object.name": "仓库名", "library_object.name_required": "仓库名(必填)", "library_object.slug": "ID 短链", diff --git a/src/tagstudio/resources/translations/zh_Hant.json b/src/tagstudio/resources/translations/zh_Hant.json index e7394505..ce362ef2 100644 --- a/src/tagstudio/resources/translations/zh_Hant.json +++ b/src/tagstudio/resources/translations/zh_Hant.json @@ -9,7 +9,6 @@ "app.git": "Git 提交更新", "app.pre_release": "預先發布版本", "app.title": "{base_title} - 文件庫「{library_dir}」", - "color_manager.title": "管理標籤顏色", "color.color_border": "在邊框使用第二個顏色", "color.confirm_delete": "您確定要刪除「{color_name}」嗎?", "color.delete": "刪除顏色", @@ -19,10 +18,11 @@ "color.namespace.delete.title": "刪除顏色命名空間", "color.new": "新增顏色", "color.placeholder": "顏色", - "color.primary_required": "主要顏色 (必填)", "color.primary": "主要顏色", + "color.primary_required": "主要顏色 (必填)", "color.secondary": "次要顏色", "color.title.no_color": "無顏色", + "color_manager.title": "管理標籤顏色", "dependency.missing.title": "未找到 {dependency}", "drop_import.description": "以下檔案與文件庫中已存在的檔案路徑重複", "drop_import.duplicates_choice.plural": "以下 {count} 個檔案與文件庫中已存在的檔案路徑重複", @@ -36,28 +36,28 @@ "edit.copy_fields": "複製欄位", "edit.paste_fields": "貼上欄位", "edit.tag_manager": "管理標籤", - "entries.duplicate.merge.label": "正在合併重複項目...", "entries.duplicate.merge": "合併重複項目", + "entries.duplicate.merge.label": "正在合併重複項目...", "entries.duplicate.refresh": "重新整理重複項目", "entries.duplicates.description": "重複項目的定義為多個項目指向硬碟中的同一個檔案。合併這些重複項目會將其所有的標籤和元資料合併為一個單獨的項目。這些並不是重複的檔案,重複的檔案是 TagStudio 以外的重複檔案。", + "entries.generic.remove.removing": "正在刪除項目", + "entries.mirror": "鏡像 (&M)", "entries.mirror.confirmation": "您確定要鏡像 {count} 個項目嗎?", "entries.mirror.label": "正在鏡像 {idx}/{total} 個項目...", "entries.mirror.title": "鏡像項目", "entries.mirror.window_title": "鏡像項目", - "entries.mirror": "鏡像 (&M)", + "entries.remove.plural.confirm": "您確定要刪除 {count} 個項目嗎?", "entries.running.dialog.new_entries": "正在加入 {total} 個新檔案項目...", "entries.running.dialog.title": "正在加入新檔案項目", "entries.tags": "標籤", - "entries.remove.plural.confirm": "您確定要刪除 {count} 個項目嗎?", - "entries.generic.remove.removing": "正在刪除項目", "entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。

您可以透過搜尋您的檔案來讓未連接的項目自動重新連接,或者自動刪除這些未連接項目。", - "entries.unlinked.unlinked_count": "未連接項目:{count}", "entries.unlinked.relink.attempting": "正在嘗試重新連接 {index}/{unlinked_count} 個項目,已成功重新連接 {fixed_count} 個", "entries.unlinked.relink.manual": "手動重新連接 (&M)", "entries.unlinked.relink.title": "正在重新連接", "entries.unlinked.scanning": "正在掃描文件庫中的未連接項目...", "entries.unlinked.search_and_relink": "搜尋並重新連接 (&S)", "entries.unlinked.title": "修復未連接項目", + "entries.unlinked.unlinked_count": "未連接項目:{count}", "ffmpeg.missing.description": "未找到「FFmpeg」和/或「FFprobe」。必須安裝「FFmpeg」才能進行多媒體播放和縮圖產生。", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", "field.copy": "複製欄位", @@ -66,7 +66,6 @@ "file.date_added": "新增日期", "file.date_created": "建立日期", "file.date_modified": "修改日期", - "file.path": "檔案路徑", "file.dimensions": "尺寸", "file.duplicates.description": "TagStudio 支援匯入 DupeGuru 結果來管理重複的檔案", "file.duplicates.dupeguru.advice": "在鏡像之後,您可以使用 DupeGuru 來刪除不需要的檔案。之後,利用 TagStudio 的「修復未連接項目」功能來刪除未連接項目。", @@ -75,67 +74,68 @@ "file.duplicates.dupeguru.no_file": "沒有選擇 DupeGuru 檔案", "file.duplicates.dupeguru.open_file": "開啟 DupeGuru 結果檔案", "file.duplicates.fix": "修復重複的檔案", - "file.duplicates.matches_uninitialized": "重複檔案:無", "file.duplicates.matches": "重複檔案:{count}", - "file.duplicates.mirror_entries": "鏡像項目 (&M)", + "file.duplicates.matches_uninitialized": "重複檔案:無", "file.duplicates.mirror.description": "鏡像每個重複配對集的項目資料,合併所有資料,同時不移除或重複欄位。此操作不會刪除任何檔案或資料。", + "file.duplicates.mirror_entries": "鏡像項目 (&M)", "file.duration": "長度", "file.not_found": "未法找到檔案", - "file.open_file_with": "使用指定程式開啟", "file.open_file": "開啟檔案", + "file.open_file_with": "使用指定程式開啟", "file.open_location.generic": "在檔案管理員中顯示", "file.open_location.mac": "在 Finder 中顯示", "file.open_location.windows": "在檔案總管中顯示", + "file.path": "檔案路徑", "folders_to_tags.close_all": "關閉全部", "folders_to_tags.converting": "正在轉換資料夾為標籤", "folders_to_tags.description": "根據資料夾結構建立標籤並套用到您的項目上。\n以下結構顯示了所有將被建立的標籤和這些標籤會被套用到哪些項目上。", "folders_to_tags.open_all": "開啟全部", "folders_to_tags.title": "從資料夾建立標籤", "generic.add": "新增", - "generic.apply_alt": "套用 (&A)", "generic.apply": "套用", - "generic.cancel_alt": "取消 (&C)", + "generic.apply_alt": "套用 (&A)", "generic.cancel": "取消", + "generic.cancel_alt": "取消 (&C)", "generic.close": "關閉", "generic.continue": "繼續", "generic.copy": "複製", "generic.cut": "剪下", - "generic.delete_alt": "刪除 (&D)", "generic.delete": "刪除", - "generic.done_alt": "完成 (&D)", + "generic.delete_alt": "刪除 (&D)", "generic.done": "完成", - "generic.edit_alt": "編輯 (&E)", + "generic.done_alt": "完成 (&D)", "generic.edit": "編輯", + "generic.edit_alt": "編輯 (&E)", "generic.filename": "檔案名稱", "generic.missing": "遺失", "generic.navigation.back": "返回", "generic.navigation.next": "下一個", "generic.none": "無", - "generic.overwrite_alt": "覆寫 (&O)", "generic.overwrite": "覆寫", + "generic.overwrite_alt": "覆寫 (&O)", "generic.paste": "貼上", "generic.recent_libraries": "最近使用的文件庫", - "generic.rename_alt": "重新命名 (&R)", "generic.rename": "重新命名", + "generic.rename_alt": "重新命名 (&R)", "generic.reset": "重設", "generic.save": "儲存", - "generic.skip_alt": "跳過 (&S)", "generic.skip": "跳過", + "generic.skip_alt": "跳過 (&S)", + "home.search": "搜尋", "home.search_entries": "搜尋項目", "home.search_library": "搜尋文件庫", "home.search_tags": "搜尋標籤", - "home.search": "搜尋", + "home.thumbnail_size": "縮圖大小", "home.thumbnail_size.extra_large": "特大縮圖", "home.thumbnail_size.large": "大縮圖", "home.thumbnail_size.medium": "中縮圖", "home.thumbnail_size.mini": "迷你縮圖", "home.thumbnail_size.small": "小縮圖", - "home.thumbnail_size": "縮圖大小", "json_migration.checking_for_parity": "正在檢查一致性...", "json_migration.creating_database_tables": "正在建立資料庫表格...", "json_migration.description": "
開啟並預覽文件庫遷移過程。除非您按下「完成遷移」,否則被遷移的文件庫不會被使用。

文件庫資料應該是一致的或者要有個「已一致」標籤。不一致的資料會以紅色顯示並會有「(!)」標示在旁邊。
對於較大的文件庫,這個過程可能會花到幾分鐘以上。
", - "json_migration.discrepancies_found.description": "原始和被遷移的文件庫格式出現差異。請檢查並決定是否要繼續遷移。", "json_migration.discrepancies_found": "找到文件庫差異", + "json_migration.discrepancies_found.description": "原始和被遷移的文件庫格式出現差異。請檢查並決定是否要繼續遷移。", "json_migration.finish_migration": "完成遷移", "json_migration.heading.aliases": "別名:", "json_migration.heading.colors": "顏色:", @@ -149,31 +149,31 @@ "json_migration.heading.shorthands": "簡寫:", "json_migration.info.description": "TagStudio 版本9.4 以下的文件庫要被轉換至9.5 以上版本的格式。

請注意!

  • 您現在的文件庫不會被刪除
  • 您個人的檔案不會被刪除、移動或變更
  • 新的 9.5 以上版本儲存格式不能在 9.5 版本以前的 TagStudio 開啟

變更內容:

  • 「變遷欄位」被「標籤類別」取代。現在,標籤會被直接加入至檔案項目。然後在標籤編輯選單會根據有「是一個類別」標示的父標籤被自動分類至不同的類別。任何標籤可以被標示為一個類別,而在其之下的子標籤會自己分類至被標示為類別的父標籤底下。
  • 標籤顏色有經過調整和擴大,有些顏色又被重新命名或合併,但所有的顏色仍會轉換為完全一致或相近的顏色
    ", "json_migration.migrating_files_entries": "正在遷移 {entries:,d} 個項目...", - "json_migration.migration_complete_with_discrepancies": "遷移完畢,找到差異", "json_migration.migration_complete": "遷移完成!", + "json_migration.migration_complete_with_discrepancies": "遷移完畢,找到差異", "json_migration.start_and_preview": "開啟並預覽", + "json_migration.title": "保存格式遷移:「{path}」", "json_migration.title.new_lib": "

    9.5 版本以上文件庫

    ", "json_migration.title.old_lib": "

    9.4 版本文件庫

    ", - "json_migration.title": "保存格式遷移:「{path}」", "landing.open_create_library": "開啟/建立文件庫 {shortcut}", - "library_info.stats.entries": "項目:", - "library_info.stats.fields": "欄位:", - "library_info.stats.tags": "標籤:", - "library_object.name_required": "名稱 (必填)", - "library_object.name": "名稱", - "library_object.slug_required": "ID Slug (必填)", - "library_object.slug": "ID Slug", "library.field.add": "新增欄位", "library.field.confirm_remove": "您確定要刪除「{name}」欄位嗎?", "library.field.mixed_data": "混合資料", "library.field.remove": "刪除欄位", "library.missing": "文件庫路徑遺失", "library.name": "文件庫", - "library.refresh.scanning_preparing": "正在掃描目錄尋找新檔案...\n準備中...", "library.refresh.scanning.plural": "正在掃描目錄尋找新檔案...\n已搜尋 {searched_count} 個檔案,找到 {found_count} 個新檔案", "library.refresh.scanning.singular": "正在掃描目錄尋找新檔案...\n已搜尋 {searched_count} 個檔案,找到 {found_count} 個新檔案", + "library.refresh.scanning_preparing": "正在掃描目錄尋找新檔案...\n準備中...", "library.refresh.title": "重新整理目錄", "library.scan_library.title": "掃描文件庫", + "library_info.stats.entries": "項目:", + "library_info.stats.fields": "欄位:", + "library_info.stats.tags": "標籤:", + "library_object.name": "名稱", + "library_object.name_required": "名稱 (必填)", + "library_object.slug": "ID Slug", + "library_object.slug_required": "ID Slug (必填)", "macros.running.dialog.new_entries": "正在對 {count}/{total} 個新檔案項目執行設定的巨集指令...", "macros.running.dialog.title": "正在對新項目執行巨集指令", "media_player.autoplay": "自動播放", @@ -181,10 +181,11 @@ "menu.delete_selected_files_ambiguous": "移動檔案至「{trash_term}」", "menu.delete_selected_files_plural": "移動多個檔案至「{trash_term}」", "menu.delete_selected_files_singular": "移動檔案至「{trash_term}」", + "menu.edit": "編輯", "menu.edit.ignore_files": "忽略檔案和資料夾", "menu.edit.manage_tags": "管理標籤", "menu.edit.new_tag": "新增標籤 (&N)", - "menu.edit": "編輯", + "menu.file": "檔案 (&F)", "menu.file.clear_recent_libraries": "清除最近使用的文件庫", "menu.file.close_library": "關閉文件庫 (&C)", "menu.file.missing_library.message": "未找到文件庫(路徑:{library})", @@ -196,20 +197,19 @@ "menu.file.refresh_directories": "重新整理目錄 (&R)", "menu.file.save_backup": "儲存文件庫備份 (&S)", "menu.file.save_library": "儲存文件庫", - "menu.file": "檔案 (&F)", - "menu.help.about": "關於", "menu.help": "幫助 (&H)", - "menu.macros.folders_to_tags": "資料夾轉標籤", + "menu.help.about": "關於", "menu.macros": "巨集指令 (&M)", + "menu.macros.folders_to_tags": "資料夾轉標籤", "menu.select": "選擇", "menu.settings": "設定...", + "menu.tools": "工具 (&T)", "menu.tools.fix_duplicate_files": "修復重複檔案", "menu.tools.fix_unlinked_entries": "修復未連接項目", - "menu.tools": "工具 (&T)", "menu.view": "檢視 (&V)", "menu.window": "視窗 (&W)", - "namespace.create.description_color": "標籤顏色使用命名空間作為色彩群組。所有自訂顏色必須先被放入一個命名空間群組。", "namespace.create.description": "TagStudio 使用命名空間來區分成群的物件,如標籤或顏色,以便這些物件能被匯出或分享。以「tagstudio」開頭的命名空間是 TagStudio 內部使用的命名空間。", + "namespace.create.description_color": "標籤顏色使用命名空間作為色彩群組。所有自訂顏色必須先被放入一個命名空間群組。", "namespace.create.title": "建立命名空間", "namespace.new.button": "新增命名空間", "namespace.new.prompt": "新增一個命名空間以新增自訂顏色", @@ -220,11 +220,16 @@ "select.clear": "清除選取", "select.inverse": "反向選取", "settings.clear_thumb_cache.title": "清除縮圖快取", + "settings.dateformat.english": "英文", + "settings.dateformat.international": "國際", + "settings.dateformat.label": "日期格式", + "settings.dateformat.system": "系統", "settings.filepath.label": "檔案路徑可見性", "settings.filepath.option.full": "僅顯示絕對檔案路徑", "settings.filepath.option.name": "僅顯示檔案名稱", "settings.filepath.option.relative": "僅顯示相對檔案路徑", "settings.global": "全域設定", + "settings.hourformat.label": "24 小時制", "settings.language": "語言", "settings.library": "文件庫設定", "settings.open_library_on_start": "啟動時開啟文件庫", @@ -232,21 +237,16 @@ "settings.restart_required": "需要重新啟動 TagStudio 才能使變更生效", "settings.show_filenames_in_grid": "在網格中顯示檔案名稱", "settings.show_recent_libraries": "顯示最近使用的文件庫", - "settings.tag_click_action.label": "標籤點選動作", "settings.tag_click_action.add_to_search": "加入標籤至搜尋範圍", + "settings.tag_click_action.label": "標籤點選動作", "settings.tag_click_action.open_edit": "編輯標籤", "settings.tag_click_action.set_search": "搜尋標籤", "settings.theme.dark": "深色模式", "settings.theme.label": "主題:", "settings.theme.light": "淺色模式", "settings.theme.system": "系統主題", - "settings.dateformat.label": "日期格式", - "settings.dateformat.system": "系統", - "settings.dateformat.english": "英文", - "settings.dateformat.international": "國際", - "settings.hourformat.label": "24 小時制", - "settings.zeropadding.label": "日期補零", "settings.title": "設定", + "settings.zeropadding.label": "日期補零", "sorting.direction.ascending": "升序", "sorting.direction.descending": "降序", "splash.opening_library": "正在開啟「{library_path}」...", @@ -264,33 +264,33 @@ "status.library_version_expected": "預期版本:", "status.library_version_found": "找到版本:", "status.library_version_mismatch": "文件庫版本不符!", - "status.results_found": "找到 {count} 個結果 ({time_span})", - "status.results.invalid_syntax": "搜尋語法錯誤:", "status.results": "結果", - "tag_manager.title": "文件庫標籤", - "tag.add_to_search": "加入至搜尋範圍", - "tag.add.plural": "新增標籤", + "status.results.invalid_syntax": "搜尋語法錯誤:", + "status.results_found": "找到 {count} 個結果 ({time_span})", "tag.add": "新增標籤", + "tag.add.plural": "新增標籤", + "tag.add_to_search": "加入至搜尋範圍", "tag.aliases": "別名", "tag.all_tags": "所有標籤", "tag.choose_color": "選擇標籤顏色", "tag.color": "標籤顏色", "tag.confirm_delete": "您確定要刪除「{tag_name}」嗎?", - "tag.create_add": "建立並新增「{query}」", "tag.create": "建立標籤", + "tag.create_add": "建立並新增「{query}」", "tag.disambiguation.tooltip": "使用此標籤消除歧義", "tag.edit": "編輯標籤", "tag.is_category": "是一個類別", "tag.name": "標籤名稱", "tag.new": "新增標籤", + "tag.parent_tags": "父標籤", "tag.parent_tags.add": "新增父標籤", "tag.parent_tags.description": "在搜尋中,該可以被當作任何其他父標籤。", - "tag.parent_tags": "父標籤", "tag.remove": "刪除標籤", "tag.search_for_tag": "搜尋標籤", "tag.shorthand": "簡寫", "tag.tag_name_required": "標籤名稱 (必填)", "tag.view_limit": "檢視限制:", + "tag_manager.title": "文件庫標籤", "trash.context.ambiguous": "移動檔案至「{trash_term}」", "trash.context.plural": "移動多個檔案移至「{trash_term}」", "trash.context.singular": "移動檔案至「{trash_term}」", From 4f9d805cac0ea844f57ff342b7ed269171400ef6 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 1 Sep 2025 22:01:39 +0200 Subject: [PATCH 12/19] translations: update from Hosted Weblate (#1078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Turkish) Currently translated at 80.5% (281 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tr/ Translation: TagStudio/Strings * Translated using Weblate (Filipino) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: searinminecraft Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fil/ Translation: TagStudio/Strings * Translated using Weblate (Tamil) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/ Translation: TagStudio/Strings * Translated using Weblate (Portuguese (Brazil)) Currently translated at 71.3% (249 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/ Translation: TagStudio/Strings * Translated using Weblate (German) Currently translated at 100.0% (349 of 349 strings) Translated using Weblate (German) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Co-authored-by: Liveside Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/ Translation: TagStudio/Strings * Translated using Weblate (Russian) Currently translated at 89.6% (313 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/ Translation: TagStudio/Strings * Translated using Weblate (Japanese) Currently translated at 89.6% (313 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: wany-oh Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/ Translation: TagStudio/Strings * Translated using Weblate (Portuguese) Currently translated at 73.6% (257 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: ssantos Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt/ Translation: TagStudio/Strings * Translated using Weblate (Hungarian) Currently translated at 100.0% (349 of 349 strings) Translated using Weblate (Hungarian) Currently translated at 92.8% (324 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Co-authored-by: Szíjártó Levente Pál Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/ Translation: TagStudio/Strings * Translated using Weblate (Polish) Currently translated at 84.8% (296 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pl/ Translation: TagStudio/Strings * Translated using Weblate (Spanish) Currently translated at 90.2% (315 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/ Translation: TagStudio/Strings * Translated using Weblate (French) Currently translated at 100.0% (349 of 349 strings) Translated using Weblate (French) Currently translated at 92.8% (324 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/ Translation: TagStudio/Strings * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hant/ Translation: TagStudio/Strings * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Luoyu Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hans/ Translation: TagStudio/Strings * Translated using Weblate (Norwegian Bokmål) Currently translated at 89.1% (311 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Neemek Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nb_NO/ Translation: TagStudio/Strings * Translated using Weblate (Toki Pona) Currently translated at 84.8% (296 of 349 strings) Co-authored-by: Bee Crankson Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/ Translation: TagStudio/Strings * Translated using Weblate (Viossa) Currently translated at 82.8% (289 of 349 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com> Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/qpv/ Translation: TagStudio/Strings --------- Co-authored-by: searinminecraft Co-authored-by: Liveside Co-authored-by: wany-oh Co-authored-by: ssantos Co-authored-by: Szíjártó Levente Pál Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com> Co-authored-by: Luoyu Co-authored-by: Neemek Co-authored-by: Bee Crankson Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com> Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> --- src/tagstudio/resources/translations/de.json | 85 ++++++++++++++------ src/tagstudio/resources/translations/fr.json | 28 ++++++- src/tagstudio/resources/translations/hu.json | 26 +++++- 3 files changed, 112 insertions(+), 27 deletions(-) diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index 2ba80c61..71bb1c8f 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -12,17 +12,17 @@ "color.color_border": "Benutze Sekundärfarbe für die Umrandung", "color.confirm_delete": "Soll die Farbe \"{color_name}\" wirklich gelöscht werden?", "color.delete": "Tag löschen", - "color.import_pack": "Farb-Paket importieren", + "color.import_pack": "Farbpaket importieren", "color.name": "Name", - "color.namespace.delete.prompt": "Soll dieser Farb-Namensraum wirklich gelöscht werden? Diese aktion wird neben dem Namensraum ALLE darin enthaltenen Farben löschen!", - "color.namespace.delete.title": "Farb-Namensraum löschen", + "color.namespace.delete.prompt": "Soll dieser Farbnamespace wirklich gelöscht werden? Diese Aktion wird neben dem Namespace ALLE darin enthaltenen Farben löschen!", + "color.namespace.delete.title": "Farbnamespace löschen", "color.new": "Neue Farbe", "color.placeholder": "Farbe", "color.primary": "Primärfarbe", "color.primary_required": "Primärfarbe (erforderlich)", "color.secondary": "Sekundärfarbe", "color.title.no_color": "Keine Farbe", - "color_manager.title": "Tag-Farben verwalten", + "color_manager.title": "Tagfarben verwalten", "dependency.missing.title": "{dependency} nicht gefunden", "drop_import.description": "Die folgenden Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren", "drop_import.duplicates_choice.plural": "Die folgenden {count} Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren.", @@ -32,7 +32,7 @@ "drop_import.progress.label.singular": "Neue Dateien werden importiert...\n1 Datei importiert.{suffix}", "drop_import.progress.window_title": "Dateien Importieren", "drop_import.title": "Dateikollision(en)", - "edit.color_manager": "Tag-Farben verwalten", + "edit.color_manager": "Tagfarben verwalten", "edit.copy_fields": "Felder kopieren", "edit.paste_fields": "Felder einfügen", "edit.tag_manager": "Tags Verwalten", @@ -40,22 +40,33 @@ "entries.duplicate.merge.label": "Führe doppelte Einträge zusammen…", "entries.duplicate.refresh": "Doppelte Einträge aktualisieren", "entries.duplicates.description": "Doppelte Einträge sind definiert als mehrere Einträge, die auf dieselbe Datei auf der Festplatte verweisen. Durch das Zusammenführen dieser Einträge werden die Tags und Metadaten aller Duplikate zu einem einzigen konsolidierten Eintrag zusammengefasst. Diese sind nicht zu verwechseln mit „doppelten Dateien“, die Duplikate Ihrer Dateien selbst außerhalb von TagStudio sind.", + "entries.generic.refresh_alt": "&Aktualisieren", "entries.generic.remove.removing": "Einträge werden gelöscht", - "entries.mirror": "Spiegeln", - "entries.mirror.confirmation": "Sind Sie sich sicher, dass Sie die folgenden {count} Einträge spiegeln wollen?", + "entries.generic.remove.removing_count": "Entferne {count} Einträge...", + "entries.ignored.description": "Dateieinträge gelten als \"Ausgeblendet\", wenn sie der Bibliothek hinzugefügt wurden, bevor die Ausblendregelen (in der Datei '.ts_ignore') diese exkludiert hat. Ausgeblendete Dateien bleiben Teil der Bibliothek, damit beim Aktualisieren der Ausblendregeln keine Daten verloren gehen.", + "entries.ignored.ignored_count": "Ausgeblendete Einträge: {count}", + "entries.ignored.remove": "Entferne ausgeblendete Einträge", + "entries.ignored.remove_alt": "Entfer&ne ausgeblendete Einträge", + "entries.ignored.scanning": "Dursuche die Bibliothek für ausgeblendete Einträge...", + "entries.ignored.title": "Repariere ausgeblendete Einträge", + "entries.mirror": "&Spiegeln", + "entries.mirror.confirmation": "Sollen folgende {count} Einträge gespiegelt werden?", "entries.mirror.label": "Spiegele {idx}/{total} Einträge...", "entries.mirror.title": "Einträge werden gespiegelt", "entries.mirror.window_title": "Einträge spiegeln", - "entries.remove.plural.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?", + "entries.remove.plural.confirm": "Sollen die folgenden {count} Einträge gelöscht werden? Es werden keine Dateien auf der Festplatte gelöscht.", + "entries.remove.singular.confirm": "Soll dieser Eintrag von der Bibliothek entfernt werden? Es werden keine Dateien auf der Festplatte gelöscht.", "entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...", "entries.running.dialog.title": "Füge neue Dateieinträge hinzu", "entries.tags": "Tags", "entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.

    Nicht verknüpfte Einträge können durch das Durchsuchen Ihrer Verzeichnisse automatisch neu verknüpft, vom Benutzer manuell neu verknüpft oder auf Wunsch gelöscht werden.", - "entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge wieder zu verknüpfen, {fixed_count} bereits erfolgreich wieder verknüpft", + "entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge neu zu verknüpfen, {fixed_count} bereits erfolgreich neu verknüpft", "entries.unlinked.relink.manual": "&Manuell Neuverknüpfen", - "entries.unlinked.relink.title": "Einträge werden neuverknüpft", + "entries.unlinked.relink.title": "Einträge werden neu verknüpft", + "entries.unlinked.remove": "Entferne nicht verknüpfte Einträge", + "entries.unlinked.remove_alt": "Entfer&ne nicht verknüpfte Einträge", "entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...", - "entries.unlinked.search_and_relink": "&Suchen && Neuverbinden", + "entries.unlinked.search_and_relink": "&Suchen && Neuverknüpfen", "entries.unlinked.title": "Unverknüpfte Einträge reparieren", "entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}", "ffmpeg.missing.description": "FFmpeg und/oder FFprobe wurden nicht gefunden. FFmpeg ist für multimediale Wiedergabe und Thumbnails vonnöten.", @@ -66,18 +77,18 @@ "file.date_added": "Hinzufügungsdatum", "file.date_created": "Erstellungsdatum", "file.date_modified": "Datum geändert", - "file.dimensions": "Abmessungen", + "file.dimensions": "Dimensionen", "file.duplicates.description": "TagStudio unterstützt das Importieren von DupeGuru-Ergebnissen um Dateiduplikate zu verwalten.", - "file.duplicates.dupeguru.advice": "Nach dem Kopiervorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.", + "file.duplicates.dupeguru.advice": "Nach dem Spiegelvorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.", "file.duplicates.dupeguru.file_extension": "DupeGuru-Dateien (*.dupeguru)", - "file.duplicates.dupeguru.load_file": "DupeGuru-Datei auswäh&len", + "file.duplicates.dupeguru.load_file": "&DupeGuru-Datei laden", "file.duplicates.dupeguru.no_file": "Keine DupeGuru-Datei ausgewählt", "file.duplicates.dupeguru.open_file": "DupeGuru Ergebnisdatei öffnen", - "file.duplicates.fix": "Duplizierte Dateien korrigieren", + "file.duplicates.fix": "Duplizierte Dateien reparieren", "file.duplicates.matches": "Übereinstimmungen mit duplizierten Dateien: {count}", "file.duplicates.matches_uninitialized": "Übereinstimmungen mit doppelten Dateien: N/A", "file.duplicates.mirror.description": "Kopiert die Eintragsdaten in jeder Duplikatsmenge, wobei alle Daten kombiniert werden ohne Felder zu entfernen oder zu duplizieren. Diese Operation wird keine Dateien oder Daten löschen.", - "file.duplicates.mirror_entries": "Einträge kopieren", + "file.duplicates.mirror_entries": "&Einträge kopieren", "file.duration": "Länge", "file.not_found": "Datei nicht gefunden", "file.open_file": "Datei öffnen", @@ -110,17 +121,21 @@ "generic.missing": "Nicht vorhanden", "generic.navigation.back": "Zurück", "generic.navigation.next": "Weiter", + "generic.no": "Nein", "generic.none": "Kein(e)", "generic.overwrite": "Überschreibem", "generic.overwrite_alt": "Überschreiben", "generic.paste": "Einfügen", "generic.recent_libraries": "Aktuelle Bibliotheken", + "generic.remove": "Entfernen", + "generic.remove_alt": "&Entfernen", "generic.rename": "Umbenennen", "generic.rename_alt": "&Umbenennen", "generic.reset": "Zurücksetzen", "generic.save": "Speichern", "generic.skip": "Überspringen", "generic.skip_alt": "&Überspringen", + "generic.yes": "Ja", "home.search": "Suchen", "home.search_entries": "Nach Einträgen suchen", "home.search_library": "Bibliothek durchsuchen", @@ -131,6 +146,7 @@ "home.thumbnail_size.medium": "Mittelgroße Vorschau", "home.thumbnail_size.mini": "Mini Vorschau", "home.thumbnail_size.small": "Kleine Vorschau", + "ignore.open_file": "Zeige die Datei \"{ts_ignore}\" auf der Festplatte", "json_migration.checking_for_parity": "Parität wird überprüft...", "json_migration.creating_database_tables": "SQL Datenbank Tabellen werden erstellt...", "json_migration.description": "
    Starte den Migrationsprozess der Bibliothek und sehe die Ergebnisse in der Vorschau an. Die migrierte Bibliothek wird nicht verwendet, bis Sie auf \"Migration abschließen\" klicken.

    Bibliotheksdaten sollten entweder übereinstimmende Werte oder das Label \"Matched\" besitzen. Werte zu denen keine Übereinstimmungen gefunden werden, werden in Rot dargestellt und erhalten das Symbol \"(!)\".
    Der Migrationsprozess kann bei größeren Bibliotheken einige Minuten in Anspruch nehmen.
", @@ -147,7 +163,7 @@ "json_migration.heading.parent_tags": "Übergeordnete Tags:", "json_migration.heading.paths": "Pfade:", "json_migration.heading.shorthands": "Kurzformen:", - "json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio-Versionen 9.4 und niedriger erstellt wurden, müssen in das neue Format v9.5+ migriert werden.

Was du wissen solltest:

  • Deine bestehenden Bibliotheksdaten werden NICHT gelöscht.
  • Deine persönlichen Dateien werden NICHT gelöscht, verschoben oder verändert.
  • Das neue Format v9.5+ kann nicht von früheren TagStudio-Versionen geöffnet werden.
", + "json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio-Versionen 9.4 und niedriger erstellt wurden, müssen in das neue Format v9.5+ migriert werden.

Was du wissen solltest:

  • Deine bestehenden Bibliotheksdaten werden NICHT gelöscht.
  • Deine persönlichen Dateien werden NICHT gelöscht, verschoben oder verändert.
  • Das neue Format v9.5+ kann nicht von früheren TagStudio-Versionen geöffnet werden.

Änderungen:

  • \"Tagfelder\" wurden mit \"Tagkategorien\" ausgetauscht. Anstatt Tags Feldern hinzuzufügen, werden diese nun direkt den Dateieinträgen hinzugefügt. Diese werden dann, basierend auf den übergeordneten Tags, welche mit der neuen Eigenschaft \"Ist Kategorie\" im Tag Menü markiert wurden, sortiert. Jeder Tag kann als Kategorie deklariert werden und untergeordnete Tags sortieren sich automatisch unter dem übergeordneten Tag, welcher als Kategorie markiert wurde. Der Tag \"Favorit\" und \"Archiviert\" basieren nun standartmäßig auf dem \"Meta Tag\", welcher standartmäßig als Kategorie deklariert ist.
  • Tagfarben wurden angepasst und erweitert. Einige Farben wurden umbenannt, oder konsolidiert. Alle bisherigen Tagfarben wandeln sich in die exakten, oder nächstmöglichen Farben in v9.5 um.
    ", "json_migration.migrating_files_entries": "Migriere {entries:,d} Dateieinträge...", "json_migration.migration_complete": "Migration abgeschlossen!", "json_migration.migration_complete_with_discrepancies": "Migration abgeschlossen, Diskrepanzen gefunden", @@ -164,15 +180,27 @@ "library.name": "Bibliothek", "library.refresh.scanning.plural": "Durchsuche Verzeichnisse nach neuen Dateien...\n{searched_count} Dateien durchsucht, {found_count} neue Dateien gefunden", "library.refresh.scanning.singular": "Durchsuche Verzeichnisse nach neuen Dateien...\n{searched_count} Datei durchsucht, {found_count} neue Datei gefunden", + "library_info.cleanup.backups": "Bibliotheks-Backups:", + "library_info.cleanup.dupe_files": "Doppelte Dateien:", + "library_info.cleanup.ignored": "Ausgeblendete Einträge:", + "library_info.cleanup.legacy_json": "Übriggebliebene Legacybibliotheken:", + "library_info.cleanup.unlinked": "Nicht verlinkte Einträge:", + "library_info.cleanup": "Aufräumen", + "library_info.stats.colors": "Tagfarben:", + "library_info.stats.entries": "Einträge:", + "library_info.stats.fields": "Felder:", + "library_info.stats.macros": "Macros:", + "library_info.stats.namespaces": "Namespaces:", + "library_info.stats.tags": "Tags:", + "library_info.stats": "Statistiken", + "library_info.title": "Bibliothek '{library_dir}'", + "library_info.version": "Formatsversion der Bibliothek: {version}", + "library_object.name_required": "Name (erforderlich)", + "library_object.name": "Name", + "library_object.slug": "ID Schlüssel", "library.refresh.scanning_preparing": "Überprüfe Verzeichnisse auf neue Dateien...\nBereite vor...", "library.refresh.title": "Verzeichnisse werden aktualisiert", "library.scan_library.title": "Bibliothek wird scannen", - "library_info.stats.entries": "Einträge:", - "library_info.stats.fields": "Felder:", - "library_info.stats.tags": "Tags:", - "library_object.name": "Name", - "library_object.name_required": "Name (erforderlich)", - "library_object.slug": "ID Schlüssel", "library_object.slug_required": "ID Schlüssel (erforderlich)", "macros.running.dialog.new_entries": "Führe konfigurierte Makros für {count}/{total} neue Dateieinträge aus...", "macros.running.dialog.title": "Ausführen von Makros bei neuen Einträgen", @@ -191,6 +219,7 @@ "menu.file.missing_library.message": "Der Pfad für die Bibliothek \"{library}\" kann nicht gefunden werden.", "menu.file.missing_library.title": "Nicht vorhandene Bibliothek", "menu.file.new_library": "Neue Bibliothek", + "menu.file.open_backups_folder": "Bibliotheksbackupordner öffnen", "menu.file.open_create_library": "Bibli&othek öffnen/erstellen", "menu.file.open_library": "Bibliothek öffnen", "menu.file.open_recent_library": "Zuletzt verwendete öffnen", @@ -205,16 +234,22 @@ "menu.settings": "Optionen...", "menu.tools": "Werkzeuge", "menu.tools.fix_duplicate_files": "Duplizierte &Dateien reparieren", + "menu.tools.fix_ignored_entries": "&Ausgeblendete Einträge reparieren", "menu.tools.fix_unlinked_entries": "&Unverknüpfte Einträge reparieren", "menu.view": "Ansicht", + "menu.view.decrease_thumbnail_size": "Thumbnailgröße verkleinern", + "menu.view.increase_thumbnail_size": "Thumbnailgröße vergrößern", + "menu.view.library_info": "Bibliotheks&informationen", "menu.window": "Fenster", - "namespace.create.description": "Namespaces werden von Tagstudio verwendet, um Gruppen von Objekten (bspw. Tags oder Farben) so darzustellen, dass sie einfach exportiert und geteilt werden können. Namespaces, die mit \"tagstudio\" beginnen sind für interne Vorgänge von Tagstudio reserviert.", + "namespace.create.description": "Namespaces werden von TagStudio verwendet, um Gruppen von Objekten (bspw. Tags oder Farben) so darzustellen, dass sie einfach exportiert und geteilt werden können. Namespaces, die mit \"tagstudio\" beginnen sind für interne Vorgänge von Tagstudio reserviert.", "namespace.create.description_color": "Tagfarben nutzen Namespaces als Farbpalettengruppen. Alle benutzerdefinierten Farben müssen erst einer Namespacegruppe zugeordnet werden.", "namespace.create.title": "Namensraum erstellen", "namespace.new.button": "Neuer Namensraum", "namespace.new.prompt": "Erstelle einen neuen Namensraum um eigene Farben hinzuzufügen!", + "preview.ignored": "Ausgeblendet", "preview.multiple_selection": "{count} Elemente ausgewählt", "preview.no_selection": "Keine Elemente ausgewählt", + "preview.unlinked": "Nicht verlinkt", "select.add_tag_to_selected": "Tag zu Ausgewähltem hinzufügen", "select.all": "Alle auswählen", "select.clear": "Auswahl leeren", @@ -228,6 +263,7 @@ "settings.filepath.option.full": "Zeige vollständigen Pfad", "settings.filepath.option.name": "Nur Dateinamen anzeigen", "settings.filepath.option.relative": "Zeige relative Pfade", + "settings.generate_thumbs": "Generiere Thumbnails", "settings.global": "Globale Einstellungen", "settings.hourformat.label": "24-Stunden Format", "settings.language": "Sprache", @@ -249,6 +285,7 @@ "settings.zeropadding.label": "Platzsparendes Datum", "sorting.direction.ascending": "Aufsteigend", "sorting.direction.descending": "Absteigend", + "sorting.mode.random": "Zufällig", "splash.opening_library": "Öffne Bibliothek \"{library_path}\"...", "status.deleted_file_plural": "{count} Dateien gelöscht!", "status.deleted_file_singular": "1 Datei gelöscht!", diff --git a/src/tagstudio/resources/translations/fr.json b/src/tagstudio/resources/translations/fr.json index 66603bca..cfc44c56 100644 --- a/src/tagstudio/resources/translations/fr.json +++ b/src/tagstudio/resources/translations/fr.json @@ -40,13 +40,22 @@ "entries.duplicate.merge.label": "Fusionner les entrées dupliquées...", "entries.duplicate.refresh": "Rafraichir les Entrées en Doublon", "entries.duplicates.description": "Les entrées dupliquées sont définies comme des entrées multiple qui pointent vers le même fichier sur le disque. Les fusionner va combiner les tags et metadatas de tous les duplicatas vers une seule entrée consolidée. Elles ne doivent pas être confondues avec les \"fichiers en doublon\", qui sont des doublons de vos fichiers en dehors de TagStudio.", + "entries.generic.refresh_alt": "&Recharger", "entries.generic.remove.removing": "Suppression des Entrées", + "entries.generic.remove.removing_count": "Suppressions de {count} entrées...", + "entries.ignored.description": "Les entrées de fichier sont considérées comme « ignorées » si elles ont été ajoutées à la bibliothèque avant que les règles d'ignorance de l'utilisateur (via le fichier « .ts_ignore ») aient été mises à jour pour les exclure. Les fichiers ignorés sont conservés dans la bibliothèque par défaut afin d'éviter toute perte accidentelle de données lors de la mise à jour des règles d'ignorance.", + "entries.ignored.ignored_count": "Entrées Ignorées : {count}", + "entries.ignored.remove": "Supprimer les entrées ignorées", + "entries.ignored.remove_alt": "Supprim&er les entrées ignorées", + "entries.ignored.scanning": "Recherche des entrées ignorées dans la bibliothèque...", + "entries.ignored.title": "Corriger les entrées ignorées", "entries.mirror": "&Refléter", "entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes ?", "entries.mirror.label": "Réplication de {idx}/{total} Entrées...", "entries.mirror.title": "Réplication des Entrées", "entries.mirror.window_title": "Entrée Miroir", - "entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?", + "entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ? Aucun fichiers sur votre disque ne sera supprimée.", + "entries.remove.singular.confirm": "Êtes-vous sûr de vouloir supprimer cette entrée de votre bibliothèque ? Aucun fichier sur le disque ne sera supprimé.", "entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...", "entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier", "entries.tags": "Tags", @@ -54,6 +63,8 @@ "entries.unlinked.relink.attempting": "Tentative de Reliage de {index}/{unlinked_count} Entrées, {fixed_count} ont été Reliées avec Succès", "entries.unlinked.relink.manual": "&Reliage Manuel", "entries.unlinked.relink.title": "Reliage des Entrées", + "entries.unlinked.remove": "Supprimer les entrées non liées", + "entries.unlinked.remove_alt": "Supprim&er les entrées non liées", "entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...", "entries.unlinked.search_and_relink": "&Rechercher && Relier", "entries.unlinked.title": "Réparation des Entrées non Liées", @@ -110,17 +121,21 @@ "generic.missing": "Manquant", "generic.navigation.back": "Retour", "generic.navigation.next": "Suivant", + "generic.no": "Non", "generic.none": "Aucun", "generic.overwrite": "Écraser", "generic.overwrite_alt": "&Écraser", "generic.paste": "Coller", "generic.recent_libraries": "Bibliothèques Récentes", + "generic.remove": "Supprimer", + "generic.remove_alt": "&Supprimer", "generic.rename": "Renommer", "generic.rename_alt": "&Renommer", "generic.reset": "Réinitialiser", "generic.save": "Sauvegarder", "generic.skip": "Passer", "generic.skip_alt": "&Passer", + "generic.yes": "Oui", "home.search": "Rechercher", "home.search_entries": "Recherche", "home.search_library": "Rechercher dans la Bibliothèque", @@ -168,6 +183,12 @@ "library.refresh.scanning_preparing": "Recherche de Nouveaux Fichiers dans les Dossiers...\nPréparation...", "library.refresh.title": "Rafraîchissement des Dossiers", "library.scan_library.title": "Balayage de la Bibliothèque", + "library_info.cleanup": "Nettoyage", + "library_info.cleanup.backups": "Sauvegardes de bibliothèque :", + "library_info.cleanup.dupe_files": "Fichiers en double :", + "library_info.cleanup.ignored": "Entrées ignorées :", + "library_info.cleanup.legacy_json": "Vestiges de la Bibliothèque :", + "library_info.cleanup.unlinked": "Entrées non liées :", "library_info.stats": "Statistiques", "library_info.stats.colors": "Couleurs de Tag :", "library_info.stats.entries": "Entrées :", @@ -176,6 +197,7 @@ "library_info.stats.namespaces": "Namespaces :", "library_info.stats.tags": "Tags :", "library_info.title": "Bibliothèque '{library_dir}'", + "library_info.version": "Version du format de bibliothèque : {version}", "library_object.name": "Nom", "library_object.name_required": "Nom (Requis)", "library_object.slug": "Identifiant unique", @@ -197,6 +219,7 @@ "menu.file.missing_library.message": "L'emplacement de la bibliothèque \"{library}\" n'a pas été trouvée.", "menu.file.missing_library.title": "Bibliothèque Manquante", "menu.file.new_library": "Nouvelle Bibliothèque", + "menu.file.open_backups_folder": "Ouvrir le dossier des sauvegardes", "menu.file.open_create_library": "&Ouvrir/Créer une Bibliothèque", "menu.file.open_library": "Ouvrir la Bibliothèque", "menu.file.open_recent_library": "Ouvrir la Bibliothèque récente", @@ -210,7 +233,8 @@ "menu.select": "Sélectionner", "menu.settings": "Paramètres...", "menu.tools": "&Outils", - "menu.tools.fix_duplicate_files": "Réparer les entrées de fichiers en double", + "menu.tools.fix_duplicate_files": "Réparer les entrées de &fichiers en double", + "menu.tools.fix_ignored_entries": "Corriger les entrées &Ignorer", "menu.tools.fix_unlinked_entries": "Réparer les entrées de fichier non liée", "menu.view": "&Vues", "menu.view.decrease_thumbnail_size": "Rétrécir les vignettes", diff --git a/src/tagstudio/resources/translations/hu.json b/src/tagstudio/resources/translations/hu.json index 7a57b57c..d8dc8732 100644 --- a/src/tagstudio/resources/translations/hu.json +++ b/src/tagstudio/resources/translations/hu.json @@ -40,13 +40,22 @@ "entries.duplicate.merge.label": "Egyező elemek egyesítése folyamatban…", "entries.duplicate.refresh": "Egyező elemek &frissítése", "entries.duplicates.description": "Ha több elem ugyanazzal a fájllal van összekapcsolva, akkor egyezőnek számítanak. Ha egyesíti őket, akkor egy olyan elem lesz létrehozva, ami az eredeti elemek összes adatát tartalmazza. Ezeket nem szabad összetéveszteni az „egyező fájlokkal”, amelyek a TagStudión kívüli azonos tartalmú fájlok.", + "entries.generic.refresh_alt": "&Frissítés", "entries.generic.remove.removing": "Elemek törlése", + "entries.generic.remove.removing_count": "{count} elem eltávolítása folyamatban…", + "entries.ignored.description": "Fájlelemek akkor lehetnek „figyelmen kívül hagyva”, ha azelőtt vették fel őket a könyvtárba, mielőtt a mindenkori szabályok szerint („.ts_ignore” fájl) kizárásra kerültek volna. A szabályok frissítésekor ezek a fájlok megmaradnak a könyvtárban a véletlen törlés kiküszöbölése érdekében.", + "entries.ignored.ignored_count": "Figyelmen kívül hagyott elemek: {count}", + "entries.ignored.remove": "Figyelmen kívül hagyott elemek eltávolítása", + "entries.ignored.remove_alt": "&Figyelmen kívül hagyott elemek eltávolítása", + "entries.ignored.scanning": "Figyelmen kívül hagyott elemek keresése a könyvtárban…", + "entries.ignored.title": "Figyelmen kívül hagyott elemek javítása", "entries.mirror": "&Tükrözés", "entries.mirror.confirmation": "Biztosan tükrözni akarja az alábbi adatokat {count} különböző elemre?", "entries.mirror.label": "{total}/{idx} elem tükrözése folyamatban…", "entries.mirror.title": "Elemek tükrözése", "entries.mirror.window_title": "Elemek tükrözése", - "entries.remove.plural.confirm": "Biztosan törölni akarja az alábbi {count} elemet?", + "entries.remove.plural.confirm": "Biztosan el akarja távolítani ezt a(z) {count} elemet a könyvtárból? A lemezen található fájl nem lesz törölve.", + "entries.remove.singular.confirm": "Biztosan el akarja távolítani ezt az elemet a könyvtárból? A lemezen található fájl nem lesz törölve.", "entries.running.dialog.new_entries": "{total} új elem felvétele folyamatban…", "entries.running.dialog.title": "Új elemek felvétele", "entries.tags": "Címkék", @@ -54,6 +63,8 @@ "entries.unlinked.relink.attempting": "{unlinked_count}/{index} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva", "entries.unlinked.relink.manual": "Új&ra összekapcsolás kézileg", "entries.unlinked.relink.title": "Elemek újra összekapcsolása", + "entries.unlinked.remove": "Kapcsolat nélküli elemek eltávolítása", + "entries.unlinked.remove_alt": "&Kapcsolat nélküli elemek eltávolítása", "entries.unlinked.scanning": "Kapcsolat nélküli elemek keresése a könyvtárban…", "entries.unlinked.search_and_relink": "&Keresés és újra összekapcsolás", "entries.unlinked.title": "Kapcsolat nélküli elemek javítása", @@ -110,17 +121,21 @@ "generic.missing": "Nem található", "generic.navigation.back": "Vissza", "generic.navigation.next": "Tovább", + "generic.no": "Nem", "generic.none": "Nincs", "generic.overwrite": "Felülírás", "generic.overwrite_alt": "&Felülírás", "generic.paste": "Beillesztés", "generic.recent_libraries": "Legutóbbi könyvtárak", + "generic.remove": "Eltávolítás", + "generic.remove_alt": "&Eltávolítás", "generic.rename": "Átnevezés", "generic.rename_alt": "&Átnevezés", "generic.reset": "Alaphelyzet", "generic.save": "Mentés", "generic.skip": "Kihagyás", "generic.skip_alt": "&Kihagyás", + "generic.yes": "Igen", "home.search": "Keresés", "home.search_entries": "Tételek keresése", "home.search_library": "Keresés a könyvtárban", @@ -168,6 +183,12 @@ "library.refresh.scanning_preparing": "Új fájlok keresése a mappákban…\nElőkészítés…", "library.refresh.title": "Könyvtárak frissítése", "library.scan_library.title": "Könyvtár vizsgálata", + "library_info.cleanup": "Megtisztítás", + "library_info.cleanup.backups": "Könyvtár biztonsági mentései:", + "library_info.cleanup.dupe_files": "Egyező fájlok:", + "library_info.cleanup.ignored": "Figyelmen kívül hagyott elemek:", + "library_info.cleanup.legacy_json": "Megmaradt örökölt könyvtár:", + "library_info.cleanup.unlinked": "Kapcsolat nélküli elemek:", "library_info.stats": "Statisztikák", "library_info.stats.colors": "Címkeszínek:", "library_info.stats.entries": "Elemek:", @@ -176,6 +197,7 @@ "library_info.stats.namespaces": "Névterek:", "library_info.stats.tags": "Címkék:", "library_info.title": "„{library_dir}” könyvtár", + "library_info.version": "Könyvtárformátum verziója: {version}", "library_object.name": "Megnevezés", "library_object.name_required": "Megnevezés (kötelező)", "library_object.slug": "Azonosító-helyőrző", @@ -197,6 +219,7 @@ "menu.file.missing_library.message": "A(z) „{library}”-könyvtár nem található.", "menu.file.missing_library.title": "Hiányzó könyvtár", "menu.file.new_library": "Új könyvtár", + "menu.file.open_backups_folder": "Biztonsági mentések mappájának megnyitása", "menu.file.open_create_library": "Könyvtár meg&nyitása/létrehozása", "menu.file.open_library": "Könyvtár megnyitása", "menu.file.open_recent_library": "&Legutóbbi könyvtárak", @@ -211,6 +234,7 @@ "menu.settings": "&Beállítások…", "menu.tools": "&Eszközök", "menu.tools.fix_duplicate_files": "&Egyező fájlok egyesítése", + "menu.tools.fix_ignored_entries": "Figyelmen &kívül hagyott elemek javítása", "menu.tools.fix_unlinked_entries": "Kapcsolat &nélküli elemek javítása", "menu.view": "&Nézet", "menu.view.decrease_thumbnail_size": "Indexkép méretének csökkentése", From 781aca27ae0b5c4347f607dee15347152fb23aa9 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:14:09 -0700 Subject: [PATCH 13/19] fix: don't flush entire changes when adding tags in bulk (#1081) * fix: don't flush entire changes when adding tags in bulk * fix: remove bumped version * fix: remove bumped version, again --- src/tagstudio/core/library/alchemy/library.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index c4becc3a..fc56f919 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1433,8 +1433,13 @@ class Library: def add_tags_to_entries( self, entry_ids: int | list[int], tag_ids: int | list[int] | set[int] - ) -> bool: - """Add one or more tags to one or more entries.""" + ) -> int: + """Add one or more tags to one or more entries. + + Returns: + The total number of tags added across all entries. + """ + total_added: int = 0 logger.info( "[Library][add_tags_to_entries]", entry_ids=entry_ids, @@ -1448,16 +1453,12 @@ class Library: for entry_id in entry_ids_: try: session.add(TagEntry(tag_id=tag_id, entry_id=entry_id)) - session.flush() + total_added += 1 + session.commit() except IntegrityError: session.rollback() - try: - session.commit() - except IntegrityError as e: - logger.warning("[Library][add_tags_to_entries]", warning=e) - session.rollback() - return False - return True + + return total_added def remove_tags_from_entries( self, entry_ids: int | list[int], tag_ids: int | list[int] | set[int] @@ -1892,12 +1893,9 @@ class Library: if not result: success = False tag_ids = [tag.id for tag in from_entry.tags] - add_result = self.add_tags_to_entries(into_entry.id, tag_ids) + self.add_tags_to_entries(into_entry.id, tag_ids) self.remove_entries([from_entry.id]) - if not add_result: - success = False - return success @property From c16445f47e10d7f4127ba5b880a75a919f8d6244 Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:14:37 +0200 Subject: [PATCH 14/19] feat: auto generation of mnemonics (#1082) --- src/tagstudio/qt/main_window.py | 142 ++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index 171cc3fd..1ab39ded 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -52,6 +52,142 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) +def remove_accelerator_marker(label: str) -> str: + """Remove existing accelerator markers (&) from a label.""" + result = "" + skip = False + for i, ch in enumerate(label): + if skip: + skip = False + continue + if ch == "&": + # escaped ampersand "&&" + if i + 1 < len(label) and label[i + 1] == "&": + result += "&" + skip = True + # otherwise skip this '&' + continue + result += ch + return result + + +# Additional weight for first character in string +FIRST_CHARACTER_EXTRA_WEIGHT = 50 +# Additional weight for the beginning of a word +WORD_BEGINNING_EXTRA_WEIGHT = 50 +# Additional weight for a 'wanted' accelerator ie string with '&' +WANTED_ACCEL_EXTRA_WEIGHT = 150 + + +def calculate_weights(text: str): + weights: dict[int, str] = {} + + pos = 0 + start_character = True + wanted_character = False + + while pos < len(text): + c = text[pos] + + # skip non typeable characters + if not c.isalnum() and c != "&": + start_character = True + pos += 1 + continue + + weight = 1 + + # add special weight to first character + if pos == 0: + weight += FIRST_CHARACTER_EXTRA_WEIGHT + elif start_character: # add weight to word beginnings + weight += WORD_BEGINNING_EXTRA_WEIGHT + start_character = False + + # add weight to characters that have an & beforehand + if wanted_character: + weight += WANTED_ACCEL_EXTRA_WEIGHT + wanted_character = False + + # add decreasing weight to left characters + if pos < 50: + weight += 50 - pos + + # try to preserve the wanted accelerators + if c == "&" and (pos != len(text) - 1 and text[pos + 1] != "&" and text[pos + 1].isalnum()): + wanted_character = True + pos += 1 + continue + + while weight in weights: + weight += 1 + + if c != "&": + weights[weight] = c + + pos += 1 + + # update our maximum weight + max_weight = 0 if len(weights) == 0 else max(weights.keys()) + return max_weight, weights + + +def insert_mnemonic(label: str, char: str) -> str: + pos = label.lower().find(char) + if pos >= 0: + return label[:pos] + "&" + label[pos:] + return label + + +def assign_mnemonics(menu: QMenu): + # Collect actions + actions = [a for a in menu.actions() if not a.isSeparator()] + + # Sequence map: mnemonic key -> QAction + sequence_to_action: dict[str, QAction] = {} + + final_text: dict[QAction, str] = {} + + actions.reverse() + + while len(actions) > 0: + action = actions.pop() + label = action.text() + _, weights = calculate_weights(label) + + chosen_char = None + + # Try candidates, starting from highest weight + for weight in sorted(weights.keys(), reverse=True): + c = weights[weight].lower() + other = sequence_to_action.get(c) + + if other is None: + chosen_char = c + sequence_to_action[c] = action + break + else: + # Compare weights with existing action + other_max, _ = calculate_weights(remove_accelerator_marker(other.text())) + if weight > other_max: + # Take over from weaker action + actions.append(other) + sequence_to_action[c] = action + chosen_char = c + + # Apply mnemonic if found + if chosen_char: + plain = remove_accelerator_marker(label) + new_label = insert_mnemonic(plain, chosen_char) + final_text[action] = new_label + else: + # No mnemonic assigned → clean text + final_text[action] = remove_accelerator_marker(label) + + for a, t in final_text.items(): + a.setText(t) + + class MainMenuBar(QMenuBar): file_menu: QMenu open_library_action: QAction @@ -167,6 +303,7 @@ class MainMenuBar(QMenuBar): self.file_menu.addSeparator() + assign_mnemonics(self.file_menu) self.addMenu(self.file_menu) def setup_edit_menu(self): @@ -294,6 +431,7 @@ class MainMenuBar(QMenuBar): self.color_manager_action.setEnabled(False) self.edit_menu.addAction(self.color_manager_action) + assign_mnemonics(self.edit_menu) self.addMenu(self.edit_menu) def setup_view_menu(self): @@ -338,6 +476,7 @@ class MainMenuBar(QMenuBar): self.view_menu.addSeparator() + assign_mnemonics(self.view_menu) self.addMenu(self.view_menu) def setup_tools_menu(self): @@ -371,6 +510,7 @@ class MainMenuBar(QMenuBar): self.clear_thumb_cache_action.setEnabled(False) self.tools_menu.addAction(self.clear_thumb_cache_action) + assign_mnemonics(self.tools_menu) self.addMenu(self.tools_menu) def setup_macros_menu(self): @@ -380,6 +520,7 @@ class MainMenuBar(QMenuBar): self.folders_to_tags_action.setEnabled(False) self.macros_menu.addAction(self.folders_to_tags_action) + assign_mnemonics(self.macros_menu) self.addMenu(self.macros_menu) def setup_help_menu(self): @@ -388,6 +529,7 @@ class MainMenuBar(QMenuBar): self.about_action = QAction(Translations["menu.help.about"], self) self.help_menu.addAction(self.about_action) + assign_mnemonics(self.help_menu) self.addMenu(self.help_menu) def rebuild_open_recent_library_menu( From 1132026aff33717476c5eb4d5c5a0b2c75686c56 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 2 Sep 2025 00:19:21 +0200 Subject: [PATCH 15/19] translations: update Hungarian (#1079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (355 of 355 strings) Translated using Weblate (Hungarian) Currently translated at 99.7% (354 of 355 strings) Translated using Weblate (Hungarian) Currently translated at 99.4% (353 of 355 strings) Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/ Translation: TagStudio/Strings Co-authored-by: Szíjártó Levente Pál --- src/tagstudio/resources/translations/hu.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tagstudio/resources/translations/hu.json b/src/tagstudio/resources/translations/hu.json index d8dc8732..51db273d 100644 --- a/src/tagstudio/resources/translations/hu.json +++ b/src/tagstudio/resources/translations/hu.json @@ -273,6 +273,12 @@ "settings.restart_required": "A módosítások érvénybeléptetéséhez
    újra kell indítani a TagStudiót.", "settings.show_filenames_in_grid": "&Fájlnevek megjelenítése rácsnézetben", "settings.show_recent_libraries": "&Legutóbbi könyvtárak megjelenítése", + "settings.splash.label": "Indítókép", + "settings.splash.option.classic": "Klasszikus (9.0)", + "settings.splash.option.default": "Alapértelmezett", + "settings.splash.option.goo_gears": "Nyílt forráskódú (9.4)", + "settings.splash.option.ninety_five": "'95-ös (9.5)", + "settings.splash.option.random": "Véletlenszerű", "settings.tag_click_action.add_to_search": "Címke hozzáfűzése a kereséshez", "settings.tag_click_action.label": "Címkére kattintási művelet", "settings.tag_click_action.open_edit": "Címke szerkesztése", From 54f9c93285caa4fe478d2e95878b2f90ad30143a Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:38:34 +0200 Subject: [PATCH 16/19] refactor: auto mnemonics (#1083) --- src/tagstudio/qt/main_window.py | 137 +---------------------------- src/tagstudio/qt/mnemonics.py | 142 +++++++++++++++++++++++++++++++ src/tagstudio/qt/translations.py | 6 +- 3 files changed, 146 insertions(+), 139 deletions(-) create mode 100644 src/tagstudio/qt/mnemonics.py diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index 1ab39ded..bc7045d2 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -38,6 +38,7 @@ from tagstudio.core.library.alchemy.enums import SortingModeEnum from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel from tagstudio.qt.flowlayout import FlowLayout from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.mnemonics import assign_mnemonics from tagstudio.qt.pagination import Pagination from tagstudio.qt.platform_strings import trash_term from tagstudio.qt.resource_manager import ResourceManager @@ -52,142 +53,6 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) -def remove_accelerator_marker(label: str) -> str: - """Remove existing accelerator markers (&) from a label.""" - result = "" - skip = False - for i, ch in enumerate(label): - if skip: - skip = False - continue - if ch == "&": - # escaped ampersand "&&" - if i + 1 < len(label) and label[i + 1] == "&": - result += "&" - skip = True - # otherwise skip this '&' - continue - result += ch - return result - - -# Additional weight for first character in string -FIRST_CHARACTER_EXTRA_WEIGHT = 50 -# Additional weight for the beginning of a word -WORD_BEGINNING_EXTRA_WEIGHT = 50 -# Additional weight for a 'wanted' accelerator ie string with '&' -WANTED_ACCEL_EXTRA_WEIGHT = 150 - - -def calculate_weights(text: str): - weights: dict[int, str] = {} - - pos = 0 - start_character = True - wanted_character = False - - while pos < len(text): - c = text[pos] - - # skip non typeable characters - if not c.isalnum() and c != "&": - start_character = True - pos += 1 - continue - - weight = 1 - - # add special weight to first character - if pos == 0: - weight += FIRST_CHARACTER_EXTRA_WEIGHT - elif start_character: # add weight to word beginnings - weight += WORD_BEGINNING_EXTRA_WEIGHT - start_character = False - - # add weight to characters that have an & beforehand - if wanted_character: - weight += WANTED_ACCEL_EXTRA_WEIGHT - wanted_character = False - - # add decreasing weight to left characters - if pos < 50: - weight += 50 - pos - - # try to preserve the wanted accelerators - if c == "&" and (pos != len(text) - 1 and text[pos + 1] != "&" and text[pos + 1].isalnum()): - wanted_character = True - pos += 1 - continue - - while weight in weights: - weight += 1 - - if c != "&": - weights[weight] = c - - pos += 1 - - # update our maximum weight - max_weight = 0 if len(weights) == 0 else max(weights.keys()) - return max_weight, weights - - -def insert_mnemonic(label: str, char: str) -> str: - pos = label.lower().find(char) - if pos >= 0: - return label[:pos] + "&" + label[pos:] - return label - - -def assign_mnemonics(menu: QMenu): - # Collect actions - actions = [a for a in menu.actions() if not a.isSeparator()] - - # Sequence map: mnemonic key -> QAction - sequence_to_action: dict[str, QAction] = {} - - final_text: dict[QAction, str] = {} - - actions.reverse() - - while len(actions) > 0: - action = actions.pop() - label = action.text() - _, weights = calculate_weights(label) - - chosen_char = None - - # Try candidates, starting from highest weight - for weight in sorted(weights.keys(), reverse=True): - c = weights[weight].lower() - other = sequence_to_action.get(c) - - if other is None: - chosen_char = c - sequence_to_action[c] = action - break - else: - # Compare weights with existing action - other_max, _ = calculate_weights(remove_accelerator_marker(other.text())) - if weight > other_max: - # Take over from weaker action - actions.append(other) - sequence_to_action[c] = action - chosen_char = c - - # Apply mnemonic if found - if chosen_char: - plain = remove_accelerator_marker(label) - new_label = insert_mnemonic(plain, chosen_char) - final_text[action] = new_label - else: - # No mnemonic assigned → clean text - final_text[action] = remove_accelerator_marker(label) - - for a, t in final_text.items(): - a.setText(t) - - class MainMenuBar(QMenuBar): file_menu: QMenu open_library_action: QAction diff --git a/src/tagstudio/qt/mnemonics.py b/src/tagstudio/qt/mnemonics.py new file mode 100644 index 00000000..3ef8bbdf --- /dev/null +++ b/src/tagstudio/qt/mnemonics.py @@ -0,0 +1,142 @@ +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMenu + + +def remove_mnemonic_marker(label: str) -> str: + """Remove existing accelerator markers (&) from a label.""" + result = "" + skip = False + for i, ch in enumerate(label): + if skip: + skip = False + continue + if ch == "&": + # escaped ampersand "&&" + if i + 1 < len(label) and label[i + 1] == "&": + result += "&" + skip = True + # otherwise skip this '&' + continue + result += ch + return result + + +# Additional weight for first character in string +FIRST_CHARACTER_EXTRA_WEIGHT = 50 +# Additional weight for the beginning of a word +WORD_BEGINNING_EXTRA_WEIGHT = 50 +# Additional weight for a 'wanted' accelerator ie string with '&' +WANTED_ACCEL_EXTRA_WEIGHT = 150 + + +def calculate_weights(text: str): + weights: dict[int, str] = {} + + pos = 0 + start_character = True + wanted_character = False + + while pos < len(text): + c = text[pos] + + # skip non typeable characters + if not c.isalnum() and c != "&": + start_character = True + pos += 1 + continue + + weight = 1 + + # add special weight to first character + if pos == 0: + weight += FIRST_CHARACTER_EXTRA_WEIGHT + elif start_character: # add weight to word beginnings + weight += WORD_BEGINNING_EXTRA_WEIGHT + start_character = False + + # add weight to characters that have an & beforehand + if wanted_character: + weight += WANTED_ACCEL_EXTRA_WEIGHT + wanted_character = False + + # add decreasing weight to left characters + if pos < 50: + weight += 50 - pos + + # try to preserve the wanted accelerators + if c == "&" and (pos != len(text) - 1 and text[pos + 1] != "&" and text[pos + 1].isalnum()): + wanted_character = True + pos += 1 + continue + + while weight in weights: + weight += 1 + + if c != "&": + weights[weight] = c + + pos += 1 + + # update our maximum weight + max_weight = 0 if len(weights) == 0 else max(weights.keys()) + return max_weight, weights + + +def insert_mnemonic(label: str, char: str) -> str: + pos = label.lower().find(char) + if pos >= 0: + return label[:pos] + "&" + label[pos:] + return label + + +def assign_mnemonics(menu: QMenu): + # Collect actions + actions = [a for a in menu.actions() if not a.isSeparator()] + + # Sequence map: mnemonic key -> QAction + sequence_to_action: dict[str, QAction] = {} + + final_text: dict[QAction, str] = {} + + actions.reverse() + + while len(actions) > 0: + action = actions.pop() + label = action.text() + _, weights = calculate_weights(label) + + chosen_char = None + + # Try candidates, starting from highest weight + for weight in sorted(weights.keys(), reverse=True): + c = weights[weight].lower() + other = sequence_to_action.get(c) + + if other is None: + chosen_char = c + sequence_to_action[c] = action + break + else: + # Compare weights with existing action + other_max, _ = calculate_weights(remove_mnemonic_marker(other.text())) + if weight > other_max: + # Take over from weaker action + actions.append(other) + sequence_to_action[c] = action + chosen_char = c + + # Apply mnemonic if found + if chosen_char: + plain = remove_mnemonic_marker(label) + new_label = insert_mnemonic(plain, chosen_char) + final_text[action] = new_label + else: + # No mnemonic assigned → clean text + final_text[action] = remove_mnemonic_marker(label) + + for a, t in final_text.items(): + a.setText(t) diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index 22bd606e..6c54d85f 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -6,6 +6,8 @@ from typing import Any import structlog import ujson +from tagstudio.qt.mnemonics import remove_mnemonic_marker + logger = structlog.get_logger(__name__) DEFAULT_TRANSLATION = "en" @@ -61,9 +63,7 @@ class Translator: self._strings = self.__get_translation_dict(lang) if system() == "Darwin": for k, v in self._strings.items(): - self._strings[k] = ( - v.replace("&&", "").replace("&", "", 1).replace("", "&&") - ) + self._strings[k] = remove_mnemonic_marker(v) def __format(self, text: str, **kwargs) -> str: try: From ce095385a8c209fc03168463aaee321962043990 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:43:10 -0700 Subject: [PATCH 17/19] chore: bump version to v9.5.4 --- pyproject.toml | 2 +- src/tagstudio/core/constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2097cb9d..31210019 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "TagStudio" description = "A User-Focused Photo & File Management System." -version = "9.5.3" +version = "9.5.4" license = "GPL-3.0-only" readme = "README.md" requires-python = ">=3.12,<3.13" diff --git a/src/tagstudio/core/constants.py b/src/tagstudio/core/constants.py index 2c8eca3b..a7f4dd5c 100644 --- a/src/tagstudio/core/constants.py +++ b/src/tagstudio/core/constants.py @@ -2,7 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -VERSION: str = "9.5.3" # Major.Minor.Patch +VERSION: str = "9.5.4" # Major.Minor.Patch VERSION_BRANCH: str = "" # Usually "" or "Pre-Release" # The folder & file names where TagStudio keeps its data relative to a library. From 25f85bf443d341560db3dc3819b7dfff737732ed Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Wed, 3 Sep 2025 02:23:14 -0700 Subject: [PATCH 18/19] fix: reorder renderer types to fix early false positives --- src/tagstudio/qt/widgets/thumb_renderer.py | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index 1c5f73d3..cd0db2fb 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -1524,8 +1524,23 @@ class ThumbRenderer(QObject): if _filepath and _filepath.is_file(): try: ext: str = _filepath.suffix.lower() if _filepath.suffix else _filepath.stem.lower() - # Images ======================================================= + # Ebooks ======================================================= if MediaCategories.is_ext_in_category( + ext, MediaCategories.EBOOK_TYPES, mime_fallback=True + ): + image = self._epub_cover(_filepath) + # Krita ======================================================== + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.KRITA_TYPES, mime_fallback=True + ): + image = self._krita_thumb(_filepath) + # VTF ========================================================== + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True + ): + image = self._vtf_thumb(_filepath) + # Images ======================================================= + elif MediaCategories.is_ext_in_category( ext, MediaCategories.IMAGE_TYPES, mime_fallback=True ): # Raw Images ----------------------------------------------- @@ -1552,11 +1567,6 @@ class ThumbRenderer(QObject): # PowerPoint Slideshow elif ext in {".pptx"}: image = self._powerpoint_thumb(_filepath) - # Krita ======================================================== - elif MediaCategories.is_ext_in_category( - ext, MediaCategories.KRITA_TYPES, mime_fallback=True - ): - image = self._krita_thumb(_filepath) # OpenDocument/OpenOffice ====================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True @@ -1590,11 +1600,6 @@ class ThumbRenderer(QObject): savable_media_type = False if image is not None: image = self._apply_overlay_color(image, UiColor.GREEN) - # Ebooks ======================================================= - elif MediaCategories.is_ext_in_category( - ext, MediaCategories.EBOOK_TYPES, mime_fallback=True - ): - image = self._epub_cover(_filepath) # Blender ====================================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.BLENDER_TYPES, mime_fallback=True @@ -1605,11 +1610,6 @@ class ThumbRenderer(QObject): ext, MediaCategories.PDF_TYPES, mime_fallback=True ): image = self._pdf_thumb(_filepath, adj_size) - # VTF ========================================================== - elif MediaCategories.is_ext_in_category( - ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True - ): - image = self._vtf_thumb(_filepath) # No Rendered Thumbnail ======================================== if not image: raise NoRendererError From ccd7ce136edd32fe469b6ba4a51098a7532c2a1a Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Wed, 3 Sep 2025 02:26:20 -0700 Subject: [PATCH 19/19] Revert "fix: reorder renderer types to fix early false positives" This reverts commit 25f85bf443d341560db3dc3819b7dfff737732ed. --- src/tagstudio/qt/widgets/thumb_renderer.py | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index cd0db2fb..1c5f73d3 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -1524,23 +1524,8 @@ class ThumbRenderer(QObject): if _filepath and _filepath.is_file(): try: ext: str = _filepath.suffix.lower() if _filepath.suffix else _filepath.stem.lower() - # Ebooks ======================================================= - if MediaCategories.is_ext_in_category( - ext, MediaCategories.EBOOK_TYPES, mime_fallback=True - ): - image = self._epub_cover(_filepath) - # Krita ======================================================== - elif MediaCategories.is_ext_in_category( - ext, MediaCategories.KRITA_TYPES, mime_fallback=True - ): - image = self._krita_thumb(_filepath) - # VTF ========================================================== - elif MediaCategories.is_ext_in_category( - ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True - ): - image = self._vtf_thumb(_filepath) # Images ======================================================= - elif MediaCategories.is_ext_in_category( + if MediaCategories.is_ext_in_category( ext, MediaCategories.IMAGE_TYPES, mime_fallback=True ): # Raw Images ----------------------------------------------- @@ -1567,6 +1552,11 @@ class ThumbRenderer(QObject): # PowerPoint Slideshow elif ext in {".pptx"}: image = self._powerpoint_thumb(_filepath) + # Krita ======================================================== + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.KRITA_TYPES, mime_fallback=True + ): + image = self._krita_thumb(_filepath) # OpenDocument/OpenOffice ====================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True @@ -1600,6 +1590,11 @@ class ThumbRenderer(QObject): savable_media_type = False if image is not None: image = self._apply_overlay_color(image, UiColor.GREEN) + # Ebooks ======================================================= + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.EBOOK_TYPES, mime_fallback=True + ): + image = self._epub_cover(_filepath) # Blender ====================================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.BLENDER_TYPES, mime_fallback=True @@ -1610,6 +1605,11 @@ class ThumbRenderer(QObject): ext, MediaCategories.PDF_TYPES, mime_fallback=True ): image = self._pdf_thumb(_filepath, adj_size) + # VTF ========================================================== + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True + ): + image = self._vtf_thumb(_filepath) # No Rendered Thumbnail ======================================== if not image: raise NoRendererError