chore: merge main into pyright-alchemy

This commit is contained in:
Travis Abendshien
2025-09-03 16:09:58 -07:00
58 changed files with 1802 additions and 737 deletions

View File

@@ -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" }

View File

@@ -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 <path>` | `-o` | Path to a TagStudio Library folder to open on start. |
| `--config-file <path>` | `-c` | Path to the TagStudio config file to load. |
| Argument | Short | Description |
| ------------------------ | ----- | ------------------------------------------------------ |
| `--cache-file <path>` | `-c` | Path to a TagStudio .ini or .plist cache file to use. |
| `--open <path>` | `-o` | Path to a TagStudio Library folder to open on start. |
| `--settings-file <path>` | `-s` | Path to a TagStudio .toml global settings file to use. |
| `--version` | `-v` | Displays TagStudio version information. |

View File

@@ -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
@@ -108,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 = {

View File

@@ -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"
@@ -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",

View File

@@ -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.

View File

@@ -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,45 +13,53 @@ 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)
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)
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)

View File

@@ -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"

View File

@@ -87,6 +87,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_ID_QUERY,
TAG_CHILDREN_QUERY,
)
@@ -194,8 +196,11 @@ class Library:
folder: Folder | None = 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:
@@ -205,6 +210,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...")
@@ -321,10 +331,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,
@@ -1403,8 +1413,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,
@@ -1418,16 +1433,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]
@@ -1493,7 +1504,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,
)
@@ -1872,12 +1883,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

View File

@@ -9,7 +9,7 @@ from pathlib import Path
from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.library.alchemy.library import Entry, FieldID, Library
from tagstudio.core.utils.missing_files import logger
from tagstudio.core.utils.unlinked_registry import logger
class TagStudioCore:

View File

@@ -0,0 +1,50 @@
# 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 Entry, Library
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 = []

View File

@@ -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
@@ -10,7 +15,7 @@ from wcmatch import pathlib
from tagstudio.core.library.alchemy.library import Entry, Library
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__)

View File

@@ -13,34 +13,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)
@@ -55,26 +58,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
@@ -82,11 +85,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 = []

View File

@@ -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"<h3>{count_text}</h3>")
@override
def showEvent(self, event: QtGui.QShowEvent) -> None: # type: ignore
self.update_ignored_count()
return super().showEvent(event)

View File

@@ -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("<b>1</b>") # 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"<b>{unlinked_count}</b>")
# 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"<b>{ignored_count}</b>")
# 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"<b>{dupe_files_count}</b>")
# 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"<b>{json_library_text}</b>")
# Backups
self.backups_count_label.setText(
f"<b>{self.__backups_count}</b> ({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"<b>{self.lib.get_version(DB_VERSION_CURRENT_KEY)}</b> / {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)

View File

@@ -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
@@ -80,6 +81,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
@@ -166,6 +168,7 @@ class MainMenuBar(QMenuBar):
self.file_menu.addSeparator()
assign_mnemonics(self.file_menu)
self.addMenu(self.file_menu)
def setup_edit_menu(self):
@@ -293,6 +296,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):
@@ -337,6 +341,7 @@ class MainMenuBar(QMenuBar):
self.view_menu.addSeparator()
assign_mnemonics(self.view_menu)
self.addMenu(self.view_menu)
def setup_tools_menu(self):
@@ -349,6 +354,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)
@@ -363,6 +375,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):
@@ -372,6 +385,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):
@@ -380,6 +394,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(
@@ -510,11 +525,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 +630,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)

View File

@@ -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)

View File

@@ -29,7 +29,7 @@ from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.library import Library, 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 (
@@ -384,10 +384,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

View File

@@ -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__()

View File

@@ -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"<h3>{count_text}</h3>")
@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

View File

@@ -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)

View File

@@ -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: (

View File

@@ -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)

View File

@@ -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()

View File

@@ -1,138 +1,150 @@
{
"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"
},
"splash_95": {
"path": "qt/images/splash/95.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"
}
}

View File

@@ -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 _:

View File

@@ -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("&&", "<ESC_AMP>").replace("&", "", 1).replace("<ESC_AMP>", "&&")
)
self._strings[k] = remove_mnemonic_marker(v)
def __format(self, text: str, **kwargs) -> str:
try:

View File

@@ -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
@@ -67,6 +66,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
@@ -87,7 +89,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
@@ -178,6 +180,7 @@ class QtDriver(DriverMixin, QObject):
folders_modal: FoldersToTagsModal
about_modal: AboutModal
unlinked_modal: FixUnlinkedEntriesModal
ignored_modal: FixIgnoredEntriesModal
dupe_modal: FixDupeFilesModal
library_info_window: LibraryInfoWindow
@@ -322,10 +325,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()
@@ -501,6 +504,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)
@@ -751,6 +763,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)
@@ -1745,6 +1758,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)

View File

@@ -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)

View File

@@ -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"<h3>{Translations['library_info.cleanup']}</h3>")
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)

View File

@@ -169,9 +169,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()
@@ -179,7 +181,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:
@@ -187,6 +197,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:

View File

@@ -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,
@@ -58,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
@@ -79,7 +81,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]
@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -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,29 +40,35 @@
"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.mirror": "Spiegeln",
"entries.mirror.confirmation": "Sind Sie sich sicher, dass Sie die folgenden {count} Einträge spiegeln wollen?",
"entries.generic.refresh_alt": "&Aktualisieren",
"entries.generic.remove.removing": "Einträge werden gelöscht",
"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": "Sollen die folgenden <b>{count}</b> 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.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.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.<br><br>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.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.",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Feld kopieren",
@@ -71,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",
@@ -115,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",
@@ -136,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": "<br>Starte den Migrationsprozess der Bibliothek und sehe die Ergebnisse in der Vorschau an. Die migrierte Bibliothek wird <i>nicht</i> verwendet, bis Sie auf \"Migration abschließen\" klicken.<br><br>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 \"<b>(!)</b>\".<br><center<i>Der Migrationsprozess kann bei größeren Bibliotheken einige Minuten in Anspruch nehmen.</i></center>",
@@ -152,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 <b>9.4 und niedriger</b> erstellt wurden, müssen in das neue Format <b>v9.5+</b> migriert werden.<br><h2>Was du wissen solltest:</h2><ul><li>Deine bestehenden Bibliotheksdaten werden <b><i>NICHT</i></b> gelöscht.</li><li>Deine persönlichen Dateien werden <b><i>NICHT</i></b> gelöscht, verschoben oder verändert.</li><li>Das neue Format v9.5+ kann nicht von früheren TagStudio-Versionen geöffnet werden.</li></ul>",
"json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio-Versionen <b>9.4 und niedriger</b> erstellt wurden, müssen in das neue Format <b>v9.5+</b> migriert werden.<br><h2>Was du wissen solltest:</h2><ul><li>Deine bestehenden Bibliotheksdaten werden <b><i>NICHT</i></b> gelöscht.</li><li>Deine persönlichen Dateien werden <b><i>NICHT</i></b> gelöscht, verschoben oder verändert.</li><li>Das neue Format v9.5+ kann nicht von früheren TagStudio-Versionen geöffnet werden.</li></ul><h3>Änderungen:</h3><ul><li>\"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.</li><li>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.</li></ul><ul>",
"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",
@@ -161,9 +172,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ Bibliothek</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Bibliothek</h2>",
"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",
@@ -172,12 +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_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",
@@ -196,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",
@@ -210,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": "<b>{count}</b> 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",
@@ -233,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",
@@ -254,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!",

View File

@@ -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 <b>{count}</b> 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.<br><br>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}<br>{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": "<h2>v9.4 Library</h2>",
"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,40 @@
"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.splash.label": "Splash Screen",
"settings.splash.option.classic": "Classic (9.0)",
"settings.splash.option.default": "Default",
"settings.splash.option.goo_gears": "Open Source (9.4)",
"settings.splash.option.ninety_five": "'95 (9.5)",
"settings.splash.option.random": "Random",
"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",

View File

@@ -40,29 +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.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.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. <br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Copiar campo",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ biblioteca</h2>",
"json_migration.title.old_lib": "<h2>v9.4 biblioteca</h2>",
"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",
@@ -175,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",

View File

@@ -40,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Kopyahin ang Field",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ na Library</h2>",
"json_migration.title.old_lib": "<h2>v9.4 na Library</h2>",
"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",
@@ -175,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",

View File

@@ -40,29 +40,35 @@
"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 <b>{count}</b> 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",
"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.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é. <br><br>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.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",
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
"ffmpeg.missing.description": "FFmpeg et/ou FFprobe nont pas été trouvée. FFmpeg est nécessaire pour la lecture de média et les vignettes.",
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
"field.copy": "Copier le Champ",
@@ -115,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",
@@ -136,6 +146,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": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie <i>ne</i> sera utilisée que si vous cliquez sur \"Terminer la migration\". <br><br>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 \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
@@ -161,9 +172,6 @@
"json_migration.title.new_lib": "<h2>Bibliothèque v9.5+</h2>",
"json_migration.title.old_lib": "<h2>Bibliothèque v9.4</h2>",
"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 +183,21 @@
"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 :",
"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_info.version": "Version du format de bibliothèque : {version}",
"library_object.name": "Nom",
"library_object.name_required": "Nom (Requis)",
"library_object.slug": "Identifiant unique",
@@ -196,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",
@@ -209,11 +233,13 @@
"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",
"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.",

View File

@@ -40,29 +40,35 @@
"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 el akarja távolítani ezt a(z) <b>{count}</b> 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",
"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.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.<br><br>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.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",
"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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Mező &másolása",
@@ -115,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",
@@ -136,6 +146,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": "<br>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 <i>nem</i> lesz használatba véve.<br><br>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 „<b>(!)</b>” szimbólummal lesznek ellátva.<br><center></i>Ez a folyamat nagyobb könyvtárak esetén akár több percig is eltarthat.</i><center>",
@@ -161,9 +172,6 @@
"json_migration.title.new_lib": "<h2>9.5 és afölötti könyvtár</h2>",
"json_migration.title.old_lib": "<h2>9.4-es könyvtár</h2>",
"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 +183,21 @@
"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:",
"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_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ő",
@@ -196,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",
@@ -210,10 +234,12 @@
"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",
"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.",
@@ -247,6 +273,12 @@
"settings.restart_required": "A módosítások érvénybeléptetéséhez<br>ú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",

View File

@@ -40,29 +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.unlinked.delete": "未リンクのエントリを削除",
"entries.unlinked.delete.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?",
"entries.unlinked.delete.deleting": "エントリの削除",
"entries.unlinked.delete.deleting_count": "{count} 件中 {idx} 件の未リンクのエントリを削除しています",
"entries.unlinked.delete_alt": "未リンクのエントリを削除(&L)",
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリは未リンクとして扱われます。<br><br>未リンクのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
"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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "フィールドをコピー",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
"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": "混在データ",
@@ -175,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 スラッグ",

View File

@@ -40,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Kopier Felt",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ Bibliotek</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Bibliotek</h2>",
"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",
@@ -175,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",

View File

@@ -40,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Skopiuj pole",
@@ -159,9 +154,6 @@
"json_migration.title.new_lib": "<h2>Biblioteka v9.5+ </h2>",
"json_migration.title.old_lib": "<h2>Biblioteka v9.4</h2>",
"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",
@@ -172,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...",

View File

@@ -39,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Copiar Campo",
"field.edit": "Editar Campo",

View File

@@ -37,29 +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.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.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.<br><br>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.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",
@@ -146,9 +141,6 @@
"json_migration.title.new_lib": "<h2>Biblioteca v9.5+</h2>",
"json_migration.title.old_lib": "<h2>Biblioteca v9.4</h2>",
"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",
@@ -160,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...",

View File

@@ -40,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Mverm shiruzmafal",
@@ -157,9 +152,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ Mlafuhuomi</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Mlafuhuomi</h2>",
"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",
@@ -171,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",

View File

@@ -40,29 +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.unlinked.delete": "Удалить откреплённые записи",
"entries.unlinked.delete.confirm": "Вы уверены, что хотите удалить {count} записей?",
"entries.unlinked.delete.deleting": "Удаление записей",
"entries.unlinked.delete.deleting_count": "Удаление {idx}/{count} откреплённых записей",
"entries.unlinked.delete_alt": "&Удалить откреплённые записи",
"entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".<br><br>Откреплённые записи могут быть прикреплены обратно автоматически, либо же удалены если в них нет надобности.",
"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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Копировать поле",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>Библиотека версии 9.5+</h2>",
"json_migration.title.old_lib": "<h2>Библиотека версии 9.4</h2>",
"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": "Смешанные данные",
@@ -175,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",

View File

@@ -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...",

View File

@@ -40,29 +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.unlinked.delete": "இணைக்கப்படாத உள்ளீடுகளை நீக்கு",
"entries.unlinked.delete.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?",
"entries.unlinked.delete.deleting": "உள்ளீடுகள் நீக்கப்படுகிறது",
"entries.unlinked.delete.deleting_count": "{idx}/{count} இணைக்கப்படாத உள்ளீடுகள் நீக்கப்படுகிறது",
"entries.unlinked.delete_alt": "டி & லெட் இணைக்கப்படாத உள்ளீடுகள்",
"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.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} <br> {ffprobe}: {ffprobe_status}",
"field.copy": "நகல் புலம்",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2> V9.5+ நூலகம் </h2>",
"json_migration.title.old_lib": "<h2> V9.4 நூலகம் </h2>",
"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": "கலப்பு தரவு",
@@ -175,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": "ஐடி ச்லக்",

View File

@@ -40,29 +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.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.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.<br><br>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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "o kama jo e ma sama",
@@ -155,9 +150,6 @@
"json_migration.title.new_lib": "<h2>tomo pi ilo nanpa 9.5+</h2>",
"json_migration.title.old_lib": "<h2>tomo pi ilo nanpa 9.4</h2>",
"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",
@@ -169,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",

View File

@@ -39,29 +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.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.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.<br><br>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.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",
@@ -157,9 +152,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ Kütüphane</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Kütüphane</h2>",
"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",
@@ -171,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",

View File

@@ -40,29 +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.unlinked.delete": "删除未链接的项目",
"entries.unlinked.delete.confirm": "您确定要删除以下 {count} 个项目?",
"entries.unlinked.delete.deleting": "正在删除项目",
"entries.unlinked.delete.deleting_count": "正在删除 {idx}/{count} 个未链接的项目",
"entries.unlinked.delete_alt": "删除未链接的项目(&l)",
"entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除则会被视为未链接。<br><br>未链接的条目可能会通过搜索目录自动重新链接,或者根据需要删除。",
"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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "复制字段",
@@ -161,9 +156,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ 仓库</h2>",
"json_migration.title.old_lib": "<h2>v9.4 仓库</h2>",
"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": "混合数据",
@@ -175,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 短链",

View File

@@ -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,33 +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.unlinked.delete_alt": "刪除未連接項目",
"entries.unlinked.delete.confirm": "您確定要刪除 {count} 個項目嗎?",
"entries.unlinked.delete.deleting_count": "正在刪除 {idx}/{count} 個未連接項目",
"entries.unlinked.delete.deleting": "正在刪除項目",
"entries.unlinked.delete": "刪除未連接項目",
"entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio那麼這個項目會被歸類為「未連接」。<br><br>您可以透過搜尋您的檔案來讓未連接的項目自動重新連接,或者自動刪除這些未連接項目。",
"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.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}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "複製欄位",
@@ -71,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 的「修復未連接項目」功能來刪除未連接項目。",
@@ -80,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": "<br>開啟並預覽文件庫遷移過程。除非您按下「完成遷移」,否則被遷移的文件庫<i>不會</i>被使用。<br><br>文件庫資料應該是一致的或者要有個「已一致」標籤。不一致的資料會以紅色顯示並會有「<b>(!)</b>」標示在旁邊。<br><center><i>對於較大的文件庫,這個過程可能會花到幾分鐘以上。</i></center>",
"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": "顏色:",
@@ -154,31 +149,31 @@
"json_migration.heading.shorthands": "簡寫:",
"json_migration.info.description": "TagStudio 版本<b>9.4 以下</b>的文件庫要被轉換至<b>9.5 以上</b>版本的格式。<br><h2>請注意!</h2><ul><li>您現在的文件庫<b><i>不會被</i></b>刪除</li><li>您個人的檔案<b><i>不會被</i></b>刪除、移動或變更</li><li>新的 9.5 以上版本儲存格式不能在 9.5 版本以前的 TagStudio 開啟</li></ul><h3>變更內容:</h3><ul><li>「變遷欄位」被「標籤類別」取代。現在,標籤會被直接加入至檔案項目。然後在標籤編輯選單會根據有「是一個類別」標示的父標籤被自動分類至不同的類別。任何標籤可以被標示為一個類別,而在其之下的子標籤會自己分類至被標示為類別的父標籤底下。</li><li>標籤顏色有經過調整和擴大,有些顏色又被重新命名或合併,但所有的顏色仍會轉換為完全一致或相近的顏色</li></ul><ul>",
"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": "<h2>9.5 版本以上文件庫</h2>",
"json_migration.title.old_lib": "<h2>9.4 版本文件庫</h2>",
"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": "自動播放",
@@ -186,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}",
@@ -201,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": "新增一個命名空間以新增自訂顏色",
@@ -225,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": "啟動時開啟文件庫",
@@ -237,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}」...",
@@ -269,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}」",

View File

@@ -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]

View File

@@ -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"

View File

@@ -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: