mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
feat: add library cleanup screen and 'fix ignored files' window (#1070)
* feat(ui): add LibraryInfoWindow with statistics * feat: add library cleanup screen * fix: missing resource * tests: add basic test for resource_manager.py * feat: remove ignored files in bulk * feat: open backups folder from library info window * refactor: rename unlinked+ignored modal files * refactor: sort en.json
This commit is contained in:
committed by
GitHub
parent
7a7e1cc4bd
commit
2f4b72fd4d
@@ -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"
|
||||
|
||||
@@ -67,6 +67,8 @@ from tagstudio.core.library.alchemy.constants import (
|
||||
DB_VERSION_CURRENT_KEY,
|
||||
DB_VERSION_INITIAL_KEY,
|
||||
DB_VERSION_LEGACY_KEY,
|
||||
JSON_FILENAME,
|
||||
SQL_FILENAME,
|
||||
TAG_CHILDREN_QUERY,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.db import make_tables
|
||||
@@ -213,8 +215,11 @@ class Library:
|
||||
folder: Folder | None
|
||||
included_files: set[Path] = set()
|
||||
|
||||
SQL_FILENAME: str = "ts_library.sqlite"
|
||||
JSON_FILENAME: str = "ts_library.json"
|
||||
def __init__(self) -> None:
|
||||
self.dupe_entries_count: int = -1 # NOTE: For internal management.
|
||||
self.dupe_files_count: int = -1
|
||||
self.ignored_entries_count: int = -1
|
||||
self.unlinked_entries_count: int = -1
|
||||
|
||||
def close(self):
|
||||
if self.engine:
|
||||
@@ -224,6 +229,11 @@ class Library:
|
||||
self.folder = None
|
||||
self.included_files = set()
|
||||
|
||||
self.dupe_entries_count = -1
|
||||
self.dupe_files_count = -1
|
||||
self.ignored_entries_count = -1
|
||||
self.unlinked_entries_count = -1
|
||||
|
||||
def migrate_json_to_sqlite(self, json_lib: JsonLibrary):
|
||||
"""Migrate JSON library data to the SQLite database."""
|
||||
logger.info("Starting Library Conversion...")
|
||||
@@ -340,10 +350,10 @@ class Library:
|
||||
is_new = True
|
||||
return self.open_sqlite_library(library_dir, is_new)
|
||||
else:
|
||||
self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME
|
||||
self.storage_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME
|
||||
assert isinstance(self.storage_path, Path)
|
||||
if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()):
|
||||
json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME
|
||||
json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME
|
||||
if json_path.exists():
|
||||
return LibraryStatus(
|
||||
success=False,
|
||||
@@ -1513,7 +1523,7 @@ class Library:
|
||||
target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
|
||||
|
||||
shutil.copy2(
|
||||
self.library_dir / TS_FOLDER_NAME / self.SQL_FILENAME,
|
||||
self.library_dir / TS_FOLDER_NAME / SQL_FILENAME,
|
||||
target_path,
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from tagstudio.core.constants import TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.fields import _FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.utils.missing_files import logger
|
||||
from tagstudio.core.utils.unlinked_registry import logger
|
||||
|
||||
|
||||
class TagStudioCore:
|
||||
|
||||
51
src/tagstudio/core/utils/ignored_registry.py
Normal file
51
src/tagstudio/core/utils/ignored_registry.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IgnoredRegistry:
|
||||
"""State tracker for ignored entries."""
|
||||
|
||||
lib: Library
|
||||
ignored_entries: list[Entry] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def ignored_count(self) -> int:
|
||||
return len(self.ignored_entries)
|
||||
|
||||
def reset(self):
|
||||
self.ignored_entries.clear()
|
||||
|
||||
def refresh_ignored_entries(self) -> Iterator[int]:
|
||||
"""Track the number of entries that would otherwise be ignored by the current rules."""
|
||||
logger.info("[IgnoredRegistry] Refreshing ignored entries...")
|
||||
|
||||
self.ignored_entries = []
|
||||
library_dir: Path = unwrap(self.lib.library_dir)
|
||||
|
||||
for i, entry in enumerate(self.lib.all_entries()):
|
||||
if not Ignore.compiled_patterns:
|
||||
# If the compiled_patterns has malfunctioned, don't consider that a false positive
|
||||
yield i
|
||||
elif Ignore.compiled_patterns.match(library_dir / entry.path):
|
||||
self.ignored_entries.append(entry)
|
||||
yield i
|
||||
|
||||
def remove_ignored_entries(self) -> None:
|
||||
self.lib.remove_entries(list(map(lambda ignored: ignored.id, self.ignored_entries)))
|
||||
self.ignored_entries = []
|
||||
@@ -1,3 +1,8 @@
|
||||
# Copyright (C) 2025
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import shutil
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
@@ -11,7 +16,7 @@ from wcmatch import pathlib
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
|
||||
from tagstudio.qt.helpers.silent_popen import silent_run
|
||||
from tagstudio.qt.helpers.silent_popen import silent_run # pyright: ignore
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@@ -14,34 +14,37 @@ logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MissingRegistry:
|
||||
"""State tracker for unlinked and moved files."""
|
||||
class UnlinkedRegistry:
|
||||
"""State tracker for unlinked entries."""
|
||||
|
||||
library: Library
|
||||
lib: Library
|
||||
files_fixed_count: int = 0
|
||||
missing_file_entries: list[Entry] = field(default_factory=list)
|
||||
unlinked_entries: list[Entry] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def missing_file_entries_count(self) -> int:
|
||||
return len(self.missing_file_entries)
|
||||
def unlinked_entries_count(self) -> int:
|
||||
return len(self.unlinked_entries)
|
||||
|
||||
def refresh_missing_files(self) -> Iterator[int]:
|
||||
def reset(self):
|
||||
self.unlinked_entries.clear()
|
||||
|
||||
def refresh_unlinked_files(self) -> Iterator[int]:
|
||||
"""Track the number of entries that point to an invalid filepath."""
|
||||
logger.info("[refresh_missing_files] Refreshing missing files...")
|
||||
logger.info("[UnlinkedRegistry] Refreshing unlinked files...")
|
||||
|
||||
self.missing_file_entries = []
|
||||
for i, entry in enumerate(self.library.all_entries()):
|
||||
full_path = unwrap(self.library.library_dir) / entry.path
|
||||
self.unlinked_entries = []
|
||||
for i, entry in enumerate(self.lib.all_entries()):
|
||||
full_path = unwrap(self.lib.library_dir) / entry.path
|
||||
if not full_path.exists() or not full_path.is_file():
|
||||
self.missing_file_entries.append(entry)
|
||||
self.unlinked_entries.append(entry)
|
||||
yield i
|
||||
|
||||
def match_missing_file_entry(self, match_entry: Entry) -> list[Path]:
|
||||
def match_unlinked_file_entry(self, match_entry: Entry) -> list[Path]:
|
||||
"""Try and match unlinked file entries with matching results in the library directory.
|
||||
|
||||
Works if files were just moved to different subfolders and don't have duplicate names.
|
||||
"""
|
||||
library_dir = unwrap(self.library.library_dir)
|
||||
library_dir = unwrap(self.lib.library_dir)
|
||||
matches: list[Path] = []
|
||||
|
||||
ignore_patterns = Ignore.get_patterns(library_dir)
|
||||
@@ -56,26 +59,26 @@ class MissingRegistry:
|
||||
new_path = Path(path).relative_to(library_dir)
|
||||
matches.append(new_path)
|
||||
|
||||
logger.info("[MissingRegistry] Matches", matches=matches)
|
||||
logger.info("[UnlinkedRegistry] Matches", matches=matches)
|
||||
return matches
|
||||
|
||||
def fix_unlinked_entries(self) -> Iterator[int]:
|
||||
"""Attempt to fix unlinked file entries by finding a match in the library directory."""
|
||||
self.files_fixed_count = 0
|
||||
matched_entries: list[Entry] = []
|
||||
for i, entry in enumerate(self.missing_file_entries):
|
||||
item_matches = self.match_missing_file_entry(entry)
|
||||
for i, entry in enumerate(self.unlinked_entries):
|
||||
item_matches = self.match_unlinked_file_entry(entry)
|
||||
if len(item_matches) == 1:
|
||||
logger.info(
|
||||
"[fix_unlinked_entries]",
|
||||
"[UnlinkedRegistry]",
|
||||
entry=entry.path.as_posix(),
|
||||
item_matches=item_matches[0].as_posix(),
|
||||
)
|
||||
if not self.library.update_entry_path(entry.id, item_matches[0]):
|
||||
if not self.lib.update_entry_path(entry.id, item_matches[0]):
|
||||
try:
|
||||
match = unwrap(self.library.get_entry_full_by_path(item_matches[0]))
|
||||
entry_full = unwrap(self.library.get_entry_full(entry.id))
|
||||
self.library.merge_entries(entry_full, match)
|
||||
match = unwrap(self.lib.get_entry_full_by_path(item_matches[0]))
|
||||
entry_full = unwrap(self.lib.get_entry_full(entry.id))
|
||||
self.lib.merge_entries(entry_full, match)
|
||||
except AttributeError:
|
||||
continue
|
||||
self.files_fixed_count += 1
|
||||
@@ -83,11 +86,8 @@ class MissingRegistry:
|
||||
yield i
|
||||
|
||||
for entry in matched_entries:
|
||||
self.missing_file_entries.remove(entry)
|
||||
self.unlinked_entries.remove(entry)
|
||||
|
||||
def execute_deletion(self) -> None:
|
||||
self.library.remove_entries(
|
||||
list(map(lambda missing: missing.id, self.missing_file_entries))
|
||||
)
|
||||
|
||||
self.missing_file_entries = []
|
||||
def remove_unlinked_entries(self) -> None:
|
||||
self.lib.remove_entries(list(map(lambda unlinked: unlinked.id, self.unlinked_entries)))
|
||||
self.unlinked_entries = []
|
||||
94
src/tagstudio/qt/controller/fix_ignored_modal_controller.py
Normal file
94
src/tagstudio/qt/controller/fix_ignored_modal_controller.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -80,6 +80,7 @@ class MainMenuBar(QMenuBar):
|
||||
|
||||
tools_menu: QMenu
|
||||
fix_unlinked_entries_action: QAction
|
||||
fix_ignored_entries_action: QAction
|
||||
fix_dupe_files_action: QAction
|
||||
clear_thumb_cache_action: QAction
|
||||
|
||||
@@ -349,6 +350,13 @@ class MainMenuBar(QMenuBar):
|
||||
self.fix_unlinked_entries_action.setEnabled(False)
|
||||
self.tools_menu.addAction(self.fix_unlinked_entries_action)
|
||||
|
||||
# Fix Ignored Entries
|
||||
self.fix_ignored_entries_action = QAction(
|
||||
Translations["menu.tools.fix_ignored_entries"], self
|
||||
)
|
||||
self.fix_ignored_entries_action.setEnabled(False)
|
||||
self.tools_menu.addAction(self.fix_ignored_entries_action)
|
||||
|
||||
# Fix Duplicate Files
|
||||
self.fix_dupe_files_action = QAction(Translations["menu.tools.fix_duplicate_files"], self)
|
||||
self.fix_dupe_files_action.setEnabled(False)
|
||||
@@ -510,11 +518,8 @@ class MainWindow(QMainWindow):
|
||||
self.central_layout.setObjectName("central_layout")
|
||||
|
||||
self.setup_search_bar()
|
||||
|
||||
self.setup_extra_input_bar()
|
||||
|
||||
self.setup_content(driver)
|
||||
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
def setup_search_bar(self):
|
||||
@@ -618,7 +623,6 @@ class MainWindow(QMainWindow):
|
||||
self.content_splitter.setHandleWidth(12)
|
||||
|
||||
self.setup_entry_list(driver)
|
||||
|
||||
self.setup_preview_panel(driver)
|
||||
|
||||
self.content_splitter.setStretchFactor(0, 1)
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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: (
|
||||
119
src/tagstudio/qt/modals/remove_unlinked_modal.py
Normal file
119
src/tagstudio/qt/modals/remove_unlinked_modal.py
Normal 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)
|
||||
@@ -1,138 +1,146 @@
|
||||
{
|
||||
"splash_classic": {
|
||||
"path": "qt/images/splash/classic.png",
|
||||
"mode": "qpixmap"
|
||||
},
|
||||
"splash_goo_gears": {
|
||||
"path": "qt/images/splash/goo_gears.png",
|
||||
"mode": "qpixmap"
|
||||
},
|
||||
"icon": {
|
||||
"path": "icon.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"play_icon": {
|
||||
"path": "qt/images/play.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"pause_icon": {
|
||||
"path": "qt/images/pause.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_icon": {
|
||||
"path": "qt/images/volume.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_mute_icon": {
|
||||
"path": "qt/images/volume_mute.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"broken_link_icon": {
|
||||
"path": "qt/images/broken_link_icon.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ignored": {
|
||||
"path": "qt/images/ignored_128.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"adobe_illustrator": {
|
||||
"path": "qt/images/file_icons/adobe_illustrator.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"adobe_photoshop": {
|
||||
"path": "qt/images/file_icons/adobe_photoshop.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"affinity_photo": {
|
||||
"path": "qt/images/file_icons/affinity_photo.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"archive": {
|
||||
"path": "qt/images/file_icons/archive.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"audio": {
|
||||
"path": "qt/images/file_icons/audio.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"blender": {
|
||||
"path": "qt/images/file_icons/blender.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"database": {
|
||||
"path": "qt/images/file_icons/database.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"document": {
|
||||
"path": "qt/images/file_icons/document.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ebook": {
|
||||
"path": "qt/images/file_icons/ebook.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"file_generic": {
|
||||
"path": "qt/images/file_icons/file_generic.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"font": {
|
||||
"path": "qt/images/file_icons/font.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"image": {
|
||||
"path": "qt/images/file_icons/image.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"image_vector": {
|
||||
"path": "qt/images/file_icons/image_vector.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"material": {
|
||||
"path": "qt/images/file_icons/material.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"model": {
|
||||
"path": "qt/images/file_icons/model.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"presentation": {
|
||||
"path": "qt/images/file_icons/presentation.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"program": {
|
||||
"path": "qt/images/file_icons/program.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shader": {
|
||||
"path": "qt/images/file_icons/shader.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shortcut": {
|
||||
"path": "qt/images/file_icons/shortcut.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"spreadsheet": {
|
||||
"path": "qt/images/file_icons/spreadsheet.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"text": {
|
||||
"path": "qt/images/file_icons/text.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"video": {
|
||||
"path": "qt/images/file_icons/video.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"thumb_loading": {
|
||||
"path": "qt/images/thumb_loading.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"bxs-left-arrow": {
|
||||
"path": "qt/images/bxs-left-arrow.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"bxs-right-arrow": {
|
||||
"path": "qt/images/bxs-right-arrow.png",
|
||||
"mode": "pil"
|
||||
}
|
||||
"splash_classic": {
|
||||
"path": "qt/images/splash/classic.png",
|
||||
"mode": "qpixmap"
|
||||
},
|
||||
"splash_goo_gears": {
|
||||
"path": "qt/images/splash/goo_gears.png",
|
||||
"mode": "qpixmap"
|
||||
},
|
||||
"icon": {
|
||||
"path": "icon.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"play_icon": {
|
||||
"path": "qt/images/play.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"pause_icon": {
|
||||
"path": "qt/images/pause.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_icon": {
|
||||
"path": "qt/images/volume.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_mute_icon": {
|
||||
"path": "qt/images/volume_mute.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"broken_link_icon": {
|
||||
"path": "qt/images/broken_link_icon.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ignored": {
|
||||
"path": "qt/images/ignored_128.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"adobe_illustrator": {
|
||||
"path": "qt/images/file_icons/adobe_illustrator.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"adobe_photoshop": {
|
||||
"path": "qt/images/file_icons/adobe_photoshop.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"affinity_photo": {
|
||||
"path": "qt/images/file_icons/affinity_photo.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"archive": {
|
||||
"path": "qt/images/file_icons/archive.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"audio": {
|
||||
"path": "qt/images/file_icons/audio.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"database": {
|
||||
"path": "qt/images/file_icons/database.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"document": {
|
||||
"path": "qt/images/file_icons/document.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ebook": {
|
||||
"path": "qt/images/file_icons/ebook.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"file_generic": {
|
||||
"path": "qt/images/file_icons/file_generic.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"font": {
|
||||
"path": "qt/images/file_icons/font.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"image": {
|
||||
"path": "qt/images/file_icons/image.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"image_vector": {
|
||||
"path": "qt/images/file_icons/image_vector.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"material": {
|
||||
"path": "qt/images/file_icons/material.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"model": {
|
||||
"path": "qt/images/file_icons/model.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"presentation": {
|
||||
"path": "qt/images/file_icons/presentation.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"program": {
|
||||
"path": "qt/images/file_icons/program.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shader": {
|
||||
"path": "qt/images/file_icons/shader.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shortcut": {
|
||||
"path": "qt/images/file_icons/shortcut.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"spreadsheet": {
|
||||
"path": "qt/images/file_icons/spreadsheet.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"text": {
|
||||
"path": "qt/images/file_icons/text.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"video": {
|
||||
"path": "qt/images/file_icons/video.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"thumb_loading": {
|
||||
"path": "qt/images/thumb_loading.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"bxs-left-arrow": {
|
||||
"path": "qt/images/bxs-left-arrow.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"bxs-right-arrow": {
|
||||
"path": "qt/images/bxs-right-arrow.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"unlinked_stat": {
|
||||
"path": "qt/images/unlinked_stat.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ignored_stat": {
|
||||
"path": "qt/images/ignored_stat.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"dupe_file_stat": {
|
||||
"path": "qt/images/dupe_file_stat.png",
|
||||
"mode": "pil"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ from PySide6.QtWidgets import (
|
||||
QScrollArea,
|
||||
)
|
||||
|
||||
# this import has side-effect of import PySide resources
|
||||
import tagstudio.qt.resources_rc # noqa: F401
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
|
||||
from tagstudio.core.driver import DriverMixin
|
||||
@@ -69,6 +68,9 @@ from tagstudio.core.utils.refresh_dir import RefreshDirTracker
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.core.utils.web import strip_web_protocol
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
|
||||
# this import has side-effect of import PySide resources
|
||||
from tagstudio.qt.controller.fix_ignored_modal_controller import FixIgnoredEntriesModal
|
||||
from tagstudio.qt.controller.widgets.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controller.widgets.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.helpers.custom_runnable import CustomRunnable
|
||||
@@ -180,6 +182,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
folders_modal: FoldersToTagsModal
|
||||
about_modal: AboutModal
|
||||
unlinked_modal: FixUnlinkedEntriesModal
|
||||
ignored_modal: FixIgnoredEntriesModal
|
||||
dupe_modal: FixDupeFilesModal
|
||||
library_info_window: LibraryInfoWindow
|
||||
|
||||
@@ -503,6 +506,15 @@ class QtDriver(DriverMixin, QObject):
|
||||
create_fix_unlinked_entries_modal
|
||||
)
|
||||
|
||||
def create_ignored_entries_modal():
|
||||
if not hasattr(self, "ignored_modal"):
|
||||
self.ignored_modal = FixIgnoredEntriesModal(self.lib, self)
|
||||
self.ignored_modal.show()
|
||||
|
||||
self.main_window.menu_bar.fix_ignored_entries_action.triggered.connect(
|
||||
create_ignored_entries_modal
|
||||
)
|
||||
|
||||
def create_dupe_files_modal():
|
||||
if not hasattr(self, "dupe_modal"):
|
||||
self.dupe_modal = FixDupeFilesModal(self.lib, self)
|
||||
@@ -753,6 +765,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.menu_bar.ignore_modal_action.setEnabled(False)
|
||||
self.main_window.menu_bar.new_tag_action.setEnabled(False)
|
||||
self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(False)
|
||||
self.main_window.menu_bar.fix_ignored_entries_action.setEnabled(False)
|
||||
self.main_window.menu_bar.fix_dupe_files_action.setEnabled(False)
|
||||
self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(False)
|
||||
self.main_window.menu_bar.folders_to_tags_action.setEnabled(False)
|
||||
@@ -1747,6 +1760,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.menu_bar.ignore_modal_action.setEnabled(True)
|
||||
self.main_window.menu_bar.new_tag_action.setEnabled(True)
|
||||
self.main_window.menu_bar.fix_unlinked_entries_action.setEnabled(True)
|
||||
self.main_window.menu_bar.fix_ignored_entries_action.setEnabled(True)
|
||||
self.main_window.menu_bar.fix_dupe_files_action.setEnabled(True)
|
||||
self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(True)
|
||||
self.main_window.menu_bar.folders_to_tags_action.setEnabled(True)
|
||||
|
||||
67
src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py
Normal file
67
src/tagstudio/qt/view/widgets/fix_ignored_modal_view.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -59,6 +59,7 @@ from tagstudio.core.library.ignore import Ignore
|
||||
from tagstudio.core.media_types import MediaCategories, MediaType
|
||||
from tagstudio.core.palette import UI_COLORS, ColorType, UiColor, get_ui_color
|
||||
from tagstudio.core.utils.encoding import detect_char_encoding
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.helpers.blender_thumbnailer import blend_thumb
|
||||
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
@@ -1436,7 +1437,9 @@ class ThumbRenderer(QObject):
|
||||
if (
|
||||
image
|
||||
and Ignore.compiled_patterns
|
||||
and Ignore.compiled_patterns.match(filepath.relative_to(self.lib.library_dir))
|
||||
and Ignore.compiled_patterns.match(
|
||||
filepath.relative_to(unwrap(self.lib.library_dir))
|
||||
)
|
||||
):
|
||||
image = render_ignored((adj_size, adj_size), pixel_ratio, image)
|
||||
except TypeError:
|
||||
|
||||
BIN
src/tagstudio/resources/qt/images/dupe_file_stat.png
Normal file
BIN
src/tagstudio/resources/qt/images/dupe_file_stat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/tagstudio/resources/qt/images/ignored_stat.png
Normal file
BIN
src/tagstudio/resources/qt/images/ignored_stat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/tagstudio/resources/qt/images/unlinked_stat.png
Normal file
BIN
src/tagstudio/resources/qt/images/unlinked_stat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...",
|
||||
"entries.running.dialog.title": "Füge neue Dateieinträge hinzu",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Unverknüpfte Einträge löschen",
|
||||
"entries.unlinked.delete.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?",
|
||||
"entries.unlinked.delete.deleting": "Einträge werden gelöscht",
|
||||
"entries.unlinked.delete.deleting_count": "Lösche {idx}/{count} unverknüpfte Einträge",
|
||||
"entries.unlinked.delete_alt": "Unverknüpfte Einträge &löschen",
|
||||
"entries.remove.plural.confirm": "Sind Sie sicher, dass Sie die folgenden {count} Einträge löschen wollen?",
|
||||
"entries.generic.remove.removing": "Einträge werden gelöscht",
|
||||
"entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.<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.unlinked_count": "Unverknüpfte Einträge: {count}",
|
||||
"entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge wieder zu verknüpfen, {fixed_count} bereits erfolgreich wieder verknüpft",
|
||||
"entries.unlinked.relink.manual": "&Manuell Neuverknüpfen",
|
||||
"entries.unlinked.relink.title": "Einträge werden neuverknüpft",
|
||||
"entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...",
|
||||
|
||||
@@ -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,34 @@
|
||||
"select.clear": "Clear Selection",
|
||||
"select.inverse": "Invert Selection",
|
||||
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
|
||||
"settings.dateformat.english": "English",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "Date Format",
|
||||
"settings.dateformat.system": "System",
|
||||
"settings.filepath.label": "Filepath Visibility",
|
||||
"settings.filepath.option.full": "Show Full Paths",
|
||||
"settings.filepath.option.name": "Show Filenames Only",
|
||||
"settings.filepath.option.relative": "Show Relative Paths",
|
||||
"settings.generate_thumbs": "Thumbnail Generation",
|
||||
"settings.global": "Global Settings",
|
||||
"settings.hourformat.label": "24-Hour Time",
|
||||
"settings.language": "Language",
|
||||
"settings.library": "Library Settings",
|
||||
"settings.open_library_on_start": "Open Library on Start",
|
||||
"settings.generate_thumbs": "Thumbnail Generation",
|
||||
"settings.page_size": "Page Size",
|
||||
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
|
||||
"settings.show_filenames_in_grid": "Show Filenames in Grid",
|
||||
"settings.show_recent_libraries": "Show Recent Libraries",
|
||||
"settings.tag_click_action.label": "Tag Click Action",
|
||||
"settings.tag_click_action.add_to_search": "Add Tag to Search",
|
||||
"settings.tag_click_action.label": "Tag Click Action",
|
||||
"settings.tag_click_action.open_edit": "Edit Tag",
|
||||
"settings.tag_click_action.set_search": "Search for Tag",
|
||||
"settings.theme.dark": "Dark",
|
||||
"settings.theme.label": "Theme:",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.theme.system": "System",
|
||||
"settings.dateformat.label": "Date Format",
|
||||
"settings.dateformat.system": "System",
|
||||
"settings.dateformat.english": "English",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.hourformat.label": "24-Hour Time",
|
||||
"settings.zeropadding.label": "Date Zero-Padding",
|
||||
"settings.title": "Settings",
|
||||
"settings.zeropadding.label": "Date Zero-Padding",
|
||||
"sorting.direction.ascending": "Ascending",
|
||||
"sorting.direction.descending": "Descending",
|
||||
"sorting.mode.random": "Random",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Añadiendo {total} nuevas entradas de archivos...",
|
||||
"entries.running.dialog.title": "Añadiendo las nuevas entradas de archivos",
|
||||
"entries.tags": "Etiquetas",
|
||||
"entries.unlinked.delete": "Eliminar entradas no vinculadas",
|
||||
"entries.unlinked.delete.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?",
|
||||
"entries.unlinked.delete.deleting": "Eliminando entradas",
|
||||
"entries.unlinked.delete.deleting_count": "Eliminando {idx}/{count} entradas no vinculadas",
|
||||
"entries.unlinked.delete_alt": "Eliminar entradas no vinculadas",
|
||||
"entries.remove.plural.confirm": "¿Está seguro de que desea eliminar las siguientes {count} entradas?",
|
||||
"entries.generic.remove.removing": "Eliminando entradas",
|
||||
"entries.unlinked.description": "Cada entrada de la biblioteca está vinculada a un archivo en uno de tus directorios. Si un archivo vinculado a una entrada se mueve o se elimina fuera de TagStudio, se considerará desvinculado. <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.unlinked_count": "Entradas no vinculadas: {count}",
|
||||
"entries.unlinked.relink.attempting": "Intentando volver a vincular {index}/{unlinked_count} Entradas, {fixed_count} Reenlazado correctamente",
|
||||
"entries.unlinked.relink.manual": "&Reenlace manual",
|
||||
"entries.unlinked.relink.title": "Volver a vincular las entradas",
|
||||
"entries.unlinked.scanning": "Buscando entradas no enlazadas en la biblioteca...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Dinadagdag ang {total} Mga Bagong Entry ng File…",
|
||||
"entries.running.dialog.title": "Dinadagdag ang Mga Bagong Entry ng File",
|
||||
"entries.tags": "Mga Tag",
|
||||
"entries.unlinked.delete": "Burahin ang Mga Hindi Naka-link na Entry",
|
||||
"entries.unlinked.delete.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?",
|
||||
"entries.unlinked.delete.deleting": "Binubura ang Mga Entry",
|
||||
"entries.unlinked.delete.deleting_count": "Binubura ang {idx}/{count} (mga) Naka-unlink na Entry",
|
||||
"entries.unlinked.delete_alt": "Burahin ang Mga Naka-unlink na Entry",
|
||||
"entries.remove.plural.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?",
|
||||
"entries.generic.remove.removing": "Binubura ang Mga Entry",
|
||||
"entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.<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.unlinked_count": "Mga Naka-unlink na Entry: {count}",
|
||||
"entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {index}/{unlinked_count} Mga Entry, {fixed_count} Matagumpay na na-link muli",
|
||||
"entries.unlinked.relink.manual": "&Manwal na Pag-link Muli",
|
||||
"entries.unlinked.relink.title": "Nili-link muli ang Mga Entry",
|
||||
"entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...",
|
||||
"entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Supprimer les Entrées non Liées",
|
||||
"entries.unlinked.delete.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?",
|
||||
"entries.unlinked.delete.deleting": "Suppression des Entrées",
|
||||
"entries.unlinked.delete.deleting_count": "Suppression des Entrées non Liées {idx}/{count}",
|
||||
"entries.unlinked.delete_alt": "Supprimer les Entrées non liées",
|
||||
"entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les {count} entrées suivantes ?",
|
||||
"entries.generic.remove.removing": "Suppression des Entrées",
|
||||
"entries.unlinked.description": "Chaque entrée dans la bibliothèque est liée à un fichier dans l'un de vos dossiers. Si un fichier lié à une entrée est déplacé ou supprimé en dehors de TagStudio, il est alors considéré non lié. <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.unlinked_count": "Entrées non Liées : {count}",
|
||||
"entries.unlinked.relink.attempting": "Tentative de Reliage de {index}/{unlinked_count} Entrées, {fixed_count} ont été Reliées avec Succès",
|
||||
"entries.unlinked.relink.manual": "&Reliage Manuel",
|
||||
"entries.unlinked.relink.title": "Reliage des Entrées",
|
||||
"entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "{total} új elem felvétele folyamatban…",
|
||||
"entries.running.dialog.title": "Új elemek felvétele",
|
||||
"entries.tags": "Címkék",
|
||||
"entries.unlinked.delete": "Kapcsolat nélküli elemek törlése",
|
||||
"entries.unlinked.delete.confirm": "Biztosan törölni akarja az alábbi {count} elemet?",
|
||||
"entries.unlinked.delete.deleting": "Elemek törlése",
|
||||
"entries.unlinked.delete.deleting_count": "{count}/{idx}. kapcsolat nélküli elem törlése folyamatban…",
|
||||
"entries.unlinked.delete_alt": "&Kapcsolat nélküli elemek törlése",
|
||||
"entries.remove.plural.confirm": "Biztosan törölni akarja az alábbi {count} elemet?",
|
||||
"entries.generic.remove.removing": "Elemek törlése",
|
||||
"entries.unlinked.description": "A könyvtár minden eleme egy fájllal van összekapcsolva a számítógépen. Ha egy kapcsolt fájl a TagSudión kívül áthelyezésre vagy törésre kerül, akkor ez a kapcsolat megszakad.<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.unlinked_count": "Kapcsolat nélküli elemek: {count}",
|
||||
"entries.unlinked.relink.attempting": "{unlinked_count}/{index} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva",
|
||||
"entries.unlinked.relink.manual": "Új&ra összekapcsolás kézileg",
|
||||
"entries.unlinked.relink.title": "Elemek újra összekapcsolása",
|
||||
"entries.unlinked.scanning": "Kapcsolat nélküli elemek keresése a könyvtárban…",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...",
|
||||
"entries.running.dialog.title": "新しいファイルエントリを追加",
|
||||
"entries.tags": "タグ",
|
||||
"entries.unlinked.delete": "未リンクのエントリを削除",
|
||||
"entries.unlinked.delete.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?",
|
||||
"entries.unlinked.delete.deleting": "エントリの削除",
|
||||
"entries.unlinked.delete.deleting_count": "{count} 件中 {idx} 件の未リンクのエントリを削除しています",
|
||||
"entries.unlinked.delete_alt": "未リンクのエントリを削除(&L)",
|
||||
"entries.remove.plural.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?",
|
||||
"entries.generic.remove.removing": "エントリの削除",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリは未リンクとして扱われます。<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.unlinked_count": "未リンクのエントリ数: {count}",
|
||||
"entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件の項目を再リンク中、{fixed_count} 件を正常に再リンクしました",
|
||||
"entries.unlinked.relink.manual": "手動で再リンク(&M)",
|
||||
"entries.unlinked.relink.title": "エントリの再リンク",
|
||||
"entries.unlinked.scanning": "未リンクのエントリをライブラリ内でスキャンしています...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Legger til {total} Nye Filoppføringer...",
|
||||
"entries.running.dialog.title": "Legger til Nye Filoppføringer",
|
||||
"entries.tags": "Etiketter",
|
||||
"entries.unlinked.delete": "Slett Frakoblede Oppføringer",
|
||||
"entries.unlinked.delete.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?",
|
||||
"entries.unlinked.delete.deleting": "Sletter Oppføringer",
|
||||
"entries.unlinked.delete.deleting_count": "Sletter {idx}/{count} ulenkede oppføringer",
|
||||
"entries.unlinked.delete_alt": "&Slett Frakoblede Oppføringer",
|
||||
"entries.remove.plural.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?",
|
||||
"entries.generic.remove.removing": "Sletter Oppføringer",
|
||||
"entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.<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.unlinked_count": "Frakoblede Oppføringer: {count}",
|
||||
"entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {index}/{unlinked_count} Oppføringer, {fixed_count} Klart Gjenkoblet",
|
||||
"entries.unlinked.relink.manual": "&Manuell Gjenkobling",
|
||||
"entries.unlinked.relink.title": "Gjenkobler Oppføringer",
|
||||
"entries.unlinked.scanning": "Skanner bibliotek for ulenkede oppføringer …",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Dodawanie {total} nowych wpisów plików...",
|
||||
"entries.running.dialog.title": "Dodawanie nowych wpisów plików",
|
||||
"entries.tags": "Tagi",
|
||||
"entries.unlinked.delete": "Usuń odłączone wpisy",
|
||||
"entries.unlinked.delete.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?",
|
||||
"entries.unlinked.delete.deleting": "Usuwanie wpisów",
|
||||
"entries.unlinked.delete.deleting_count": "Usuwanie {idx}/{count} odłączonych wpisów",
|
||||
"entries.unlinked.delete_alt": "Usuń odłączone wpisy",
|
||||
"entries.remove.plural.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?",
|
||||
"entries.generic.remove.removing": "Usuwanie wpisów",
|
||||
"entries.unlinked.description": "Każdy wpis w bibliotece jest połączony z plikiem w jednym z twoich katalogów. Jeśli połączony plik jest przeniesiony poza TagStudio albo usunięty to jest uważany za odłączony.<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.unlinked_count": "Odłączone wpisy: {count}",
|
||||
"entries.unlinked.relink.attempting": "Próbowanie ponownego łączenia {index}/{unlinked_count} wpisów, {fixed_count} poprawnie połączono ponownie",
|
||||
"entries.unlinked.relink.manual": "&Ręczne ponowne łączenie",
|
||||
"entries.unlinked.relink.title": "Ponowne łączenie wpisów",
|
||||
"entries.unlinked.scanning": "Skanowanie biblioteki dla odłączonych wpisów...",
|
||||
|
||||
@@ -47,16 +47,11 @@
|
||||
"entries.running.dialog.new_entries": "A Adicionar {total} Novos Registos de Ficheiros...",
|
||||
"entries.running.dialog.title": "A Adicionar Novos Registos de Ficheiros",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Apagar Registos não Referenciados",
|
||||
"entries.unlinked.delete.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?",
|
||||
"entries.unlinked.delete.deleting": "A Apagar Registos",
|
||||
"entries.unlinked.delete.deleting_count": "A Apagar {idx}/{count} Registos Não Ligadas",
|
||||
"entries.unlinked.delete_alt": "De&letar Registos Não Ligados",
|
||||
"entries.remove.plural.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?",
|
||||
"entries.generic.remove.removing": "A Apagar Registos",
|
||||
"entries.unlinked.description": "Cada registo na biblioteca faz referência à um ficheiro numa das suas pastas. Se um ficheiro referenciado à uma entrada for movido ou apagado fora do TagStudio, ele é depois considerado não-referenciado.<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.unlinked_count": "Registos Não Referenciados: {count}",
|
||||
"entries.unlinked.relink.attempting": "A tentar referenciar {index}/{unlinked_count} Registos, {fixed_count} Referenciados com Sucesso",
|
||||
"entries.unlinked.relink.manual": "&Referência Manual",
|
||||
"entries.unlinked.relink.title": "A Referenciar Registos",
|
||||
"entries.unlinked.scanning": "A escanear biblioteca por registos não referenciados...",
|
||||
|
||||
@@ -45,16 +45,11 @@
|
||||
"entries.running.dialog.new_entries": "Adicionando {total} Novos Registros de Arquivos...",
|
||||
"entries.running.dialog.title": "Adicionando Novos Registros de Arquivos",
|
||||
"entries.tags": "Tags",
|
||||
"entries.unlinked.delete": "Deletar Registros não Referenciados",
|
||||
"entries.unlinked.delete.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?",
|
||||
"entries.unlinked.delete.deleting": "Deletando Registros",
|
||||
"entries.unlinked.delete.deleting_count": "Deletando {idx}/{count} Registros Não Linkadas",
|
||||
"entries.unlinked.delete_alt": "De&letar Registros Não Linkados",
|
||||
"entries.remove.plural.confirm": "Tem certeza que deseja deletar os seguintes {count} Registros ?",
|
||||
"entries.generic.remove.removing": "Deletando Registros",
|
||||
"entries.unlinked.description": "Cada registro na biblioteca faz referência à um arquivo em uma de suas pastas. Se um arquivo referenciado à uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não-referenciado.<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.unlinked_count": "Registros Não Referenciados: {count}",
|
||||
"entries.unlinked.relink.attempting": "Tentando referenciar {index}/{unlinked_count} Registros, {fixed_count} Referenciados com Sucesso",
|
||||
"entries.unlinked.relink.manual": "&Referência Manual",
|
||||
"entries.unlinked.relink.title": "Referenciando Registros",
|
||||
"entries.unlinked.scanning": "Escaneando bibliotecada em busca de registros não referenciados...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...",
|
||||
"entries.running.dialog.title": "Nasii neo shiruzmakaban fu mlafu ima",
|
||||
"entries.tags": "Festaretol",
|
||||
"entries.unlinked.delete": "Keste tsunaganaijena shiruzmakaban",
|
||||
"entries.unlinked.delete.confirm": "Du kestetsa afto {count} shiruzmakaban?",
|
||||
"entries.unlinked.delete.deleting": "Keste shiruzmakaban ima",
|
||||
"entries.unlinked.delete.deleting_count": "Keste {idx}/{count} tsunaganaijena shiruzmakaban ima",
|
||||
"entries.unlinked.delete_alt": "Ke&ste tsunaganaijena shiruzmakaban",
|
||||
"entries.remove.plural.confirm": "Du kestetsa afto {count} shiruzmakaban?",
|
||||
"entries.generic.remove.removing": "Keste shiruzmakaban ima",
|
||||
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore tsunaganaijena.<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.unlinked_count": "Tsunaganaijena shiruzmakaban: {count}",
|
||||
"entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen",
|
||||
"entries.unlinked.relink.manual": "&Tsunaga gen mit hant",
|
||||
"entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen",
|
||||
"entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "Добавление {total} новых записей...",
|
||||
"entries.running.dialog.title": "Добавление новых записей",
|
||||
"entries.tags": "Теги",
|
||||
"entries.unlinked.delete": "Удалить откреплённые записи",
|
||||
"entries.unlinked.delete.confirm": "Вы уверены, что хотите удалить {count} записей?",
|
||||
"entries.unlinked.delete.deleting": "Удаление записей",
|
||||
"entries.unlinked.delete.deleting_count": "Удаление {idx}/{count} откреплённых записей",
|
||||
"entries.unlinked.delete_alt": "&Удалить откреплённые записи",
|
||||
"entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?",
|
||||
"entries.generic.remove.removing": "Удаление записей",
|
||||
"entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".<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.unlinked_count": "Откреплённых записей: {count}",
|
||||
"entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно",
|
||||
"entries.unlinked.relink.manual": "&Ручная привязка",
|
||||
"entries.unlinked.relink.title": "Привязка записей",
|
||||
"entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "{total} புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது ...",
|
||||
"entries.running.dialog.title": "புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது",
|
||||
"entries.tags": "குறிச்சொற்கள்",
|
||||
"entries.unlinked.delete": "இணைக்கப்படாத உள்ளீடுகளை நீக்கு",
|
||||
"entries.unlinked.delete.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?",
|
||||
"entries.unlinked.delete.deleting": "உள்ளீடுகள் நீக்கப்படுகிறது",
|
||||
"entries.unlinked.delete.deleting_count": "{idx}/{count} இணைக்கப்படாத உள்ளீடுகள் நீக்கப்படுகிறது",
|
||||
"entries.unlinked.delete_alt": "டி & லெட் இணைக்கப்படாத உள்ளீடுகள்",
|
||||
"entries.remove.plural.confirm": "பின்வரும் உள்ளீடுகளை நிச்சயமாக நீக்க விரும்புகிறீர்களா {count}?",
|
||||
"entries.generic.remove.removing": "உள்ளீடுகள் நீக்கப்படுகிறது",
|
||||
"entries.unlinked.description": "ஒவ்வொரு நூலக நுழைவும் உங்கள் கோப்பகங்களில் ஒன்றில் ஒரு கோப்போடு இணைக்கப்பட்டுள்ளது. ஒரு நுழைவுடன் இணைக்கப்பட்ட ஒரு கோப்பு டாக்ச்டுடியோவுக்கு வெளியே நகர்த்தப்பட்டால் அல்லது நீக்கப்பட்டால், அது பின்னர் இணைக்கப்படாததாகக் கருதப்படுகிறது.",
|
||||
"entries.unlinked.missing_count.none": "இணைக்கப்படாத உள்ளீடுகள்: இதற்கில்லை",
|
||||
"entries.unlinked.missing_count.some": "இணைக்கப்படாத உள்ளீடுகள்: {count}",
|
||||
"entries.unlinked.refresh_all": "& அனைத்தையும் புதுப்பிக்கவும்",
|
||||
"entries.unlinked.relink.attempting": "{idx}/{missing_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது",
|
||||
"entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}",
|
||||
"entries.unlinked.relink.attempting": "{index}/{unlinked_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது",
|
||||
"entries.unlinked.relink.manual": "& கையேடு மறுபரிசீலனை",
|
||||
"entries.unlinked.relink.title": "உள்ளீடுகள் மீண்டும் இணைக்கப்படுகின்றது",
|
||||
"entries.unlinked.scanning": "இணைக்கப்படாத நுழைவுகளை புத்தககல்லரியில் சோதனை செய்யப்படுகிறது...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "mi pana e lipu sin {total}...",
|
||||
"entries.running.dialog.title": "mi pana e lipu sin",
|
||||
"entries.tags": "poki",
|
||||
"entries.unlinked.delete": "o weka e ijo pi ijo lon ala",
|
||||
"entries.unlinked.delete.confirm": "mi weka e ijo {count}. ni li pona anu seme?",
|
||||
"entries.unlinked.delete.deleting": "mi weka e ijo",
|
||||
"entries.unlinked.delete.deleting_count": "mi weka e ijo {idx}/{count} pi ijo lon ala",
|
||||
"entries.unlinked.delete_alt": "o weka e ijo pi &linja ala",
|
||||
"entries.remove.plural.confirm": "mi weka e ijo {count}. ni li pona anu seme?",
|
||||
"entries.generic.remove.removing": "mi weka e ijo",
|
||||
"entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.<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.unlinked_count": "ijo pi ijo lon ala: {count}",
|
||||
"entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {index}/{unlinked_count}. mi pana e ijo lon tawa ijo {fixed_count}",
|
||||
"entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo (&M)",
|
||||
"entries.unlinked.relink.title": "mi pana e ijo lon tawa ijo",
|
||||
"entries.unlinked.scanning": "mi o alasa e ijo pi ijo lon ala...",
|
||||
|
||||
@@ -47,16 +47,11 @@
|
||||
"entries.running.dialog.new_entries": "{total} Yeni Dosya Kaydı Ekleniyor...",
|
||||
"entries.running.dialog.title": "Yeni Dosya Kayıtları Ekleniyor",
|
||||
"entries.tags": "Etiketler",
|
||||
"entries.unlinked.delete": "Kopmuş Kayıtları Sil",
|
||||
"entries.unlinked.delete.confirm": "{count} tane kayıtları silmek istediğinden emin misin?",
|
||||
"entries.unlinked.delete.deleting": "Kayıtlar Siliniyor",
|
||||
"entries.unlinked.delete.deleting_count": "{idx}/{count} Kopmuş Kayıt Siliniyor",
|
||||
"entries.unlinked.delete_alt": "Kopmuş Kayıtları S&il",
|
||||
"entries.remove.plural.confirm": "{count} tane kayıtları silmek istediğinden emin misin?",
|
||||
"entries.generic.remove.removing": "Kayıtlar Siliniyor",
|
||||
"entries.unlinked.description": "Kütüphanenizdeki her bir kayıt, dizinlerinizden bir tane dosya ile eşleştirilmektedir. Eğer bir kayıta bağlı dosya TagStudio dışında taşınır veya silinirse, o dosya artık kopmuş olarak sayılır.<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.unlinked_count": "Kopmuş Kayıtlar: {count}",
|
||||
"entries.unlinked.relink.attempting": "{index}/{unlinked_count} Kayıt Yeniden Eşleştirilmeye Çalışılıyor, {fixed_count} Başarıyla Yeniden Eşleştirildi",
|
||||
"entries.unlinked.relink.manual": "&Manuel Yeniden Eşleştirme",
|
||||
"entries.unlinked.relink.title": "Kayıtlar Yeniden Eşleştiriliyor",
|
||||
"entries.unlinked.scanning": "Kütüphane, Kopmuş Kayıtlar için Taranıyor...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "正在加入 {total} 个新文件项目...",
|
||||
"entries.running.dialog.title": "正在加入新文件项目",
|
||||
"entries.tags": "标签",
|
||||
"entries.unlinked.delete": "删除未链接的项目",
|
||||
"entries.unlinked.delete.confirm": "您确定要删除以下 {count} 个项目?",
|
||||
"entries.unlinked.delete.deleting": "正在删除项目",
|
||||
"entries.unlinked.delete.deleting_count": "正在删除 {idx}/{count} 个未链接的项目",
|
||||
"entries.unlinked.delete_alt": "删除未链接的项目(&l)",
|
||||
"entries.remove.plural.confirm": "您确定要删除以下 {count} 个项目?",
|
||||
"entries.generic.remove.removing": "正在删除项目",
|
||||
"entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。<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.unlinked_count": "未链接的项目: {count}",
|
||||
"entries.unlinked.relink.attempting": "正在尝试重新链接 {index}/{unlinked_count} 个项目, {fixed_count} 个项目成功重链",
|
||||
"entries.unlinked.relink.manual": "手动重新链接(&m)",
|
||||
"entries.unlinked.relink.title": "正在重新链接项目",
|
||||
"entries.unlinked.scanning": "正在扫描仓库以寻找未链接的项目...",
|
||||
|
||||
@@ -48,16 +48,11 @@
|
||||
"entries.running.dialog.new_entries": "正在加入 {total} 個新檔案項目...",
|
||||
"entries.running.dialog.title": "正在加入新檔案項目",
|
||||
"entries.tags": "標籤",
|
||||
"entries.unlinked.delete_alt": "刪除未連接項目",
|
||||
"entries.unlinked.delete.confirm": "您確定要刪除 {count} 個項目嗎?",
|
||||
"entries.unlinked.delete.deleting_count": "正在刪除 {idx}/{count} 個未連接項目",
|
||||
"entries.unlinked.delete.deleting": "正在刪除項目",
|
||||
"entries.unlinked.delete": "刪除未連接項目",
|
||||
"entries.remove.plural.confirm": "您確定要刪除 {count} 個項目嗎?",
|
||||
"entries.generic.remove.removing": "正在刪除項目",
|
||||
"entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。<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.unlinked_count": "未連接項目:{count}",
|
||||
"entries.unlinked.relink.attempting": "正在嘗試重新連接 {index}/{unlinked_count} 個項目,已成功重新連接 {fixed_count} 個",
|
||||
"entries.unlinked.relink.manual": "手動重新連接 (&M)",
|
||||
"entries.unlinked.relink.title": "正在重新連接",
|
||||
"entries.unlinked.scanning": "正在掃描文件庫中的未連接項目...",
|
||||
|
||||
@@ -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]
|
||||
|
||||
19
tests/qt/test_resource_manager.py
Normal file
19
tests/qt/test_resource_manager.py
Normal 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"
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user