Compare commits

..

3 Commits

Author SHA1 Message Date
Travis Abendshien
e51cc14e98 docs: fix typos in README 2025-10-02 21:14:55 -07:00
Travis Abendshien
e98a16b2e7 docs: update screenshot 2025-10-02 21:14:12 -07:00
Travis Abendshien
6561c57d09 docs: update README 2025-10-02 17:53:10 -07:00
16 changed files with 73 additions and 472 deletions

View File

@@ -4,8 +4,7 @@
^^^ Summarize the changes done and why they were done above.
By submitting this pull request, you certify that you have read the
[Contributing](https://docs.tagstud.io/contributing) page on our documentation site,
or in the project's [CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/docs/contributing.md) file.
[CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md).
IMPORTANT FOR FEATURES: Please verify that a feature request or some other form
of communication with maintainers was already conducted in terms of approving.

View File

@@ -4,24 +4,21 @@ name: pytest
on: [push, pull_request]
jobs:
pytest-linux:
name: Run pytest (Linux)
pytest:
name: Run pytest
runs-on: ubuntu-24.04
steps:
- &checkout
name: Checkout repo
- name: Checkout repo
uses: actions/checkout@v4
- &setup-python
name: Setup Python
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
- &install-dependencies
name: Install Python dependencies
- name: Install Python dependencies
run: |
python -m pip install --upgrade uv
uv pip install --system .[pytest]
@@ -55,27 +52,10 @@ jobs:
name: coverage
path: coverage.xml
pytest-windows:
name: Run pytest (Windows)
runs-on: windows-2025
steps:
- *checkout
- *setup-python
- *install-dependencies
- name: Install system dependencies
run: |
choco install ripgrep
- name: Execute pytest
run: |
pytest
coverage:
name: Check coverage
runs-on: ubuntu-latest
needs: pytest-linux
needs: pytest
steps:
- name: Fetch coverage

12
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1763759067,
"narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1763835633,
"narHash": "sha256-HzxeGVID5MChuCPESuC0dlQL1/scDKu+MmzoVBJxulM=",
"lastModified": 1757487488,
"narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "050e09e091117c3d7328c7b2b7b577492c43c134",
"rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0",
"type": "github"
},
"original": {

View File

@@ -6,7 +6,7 @@
qt6,
ripgrep,
stdenv,
wrapGAppsHook3,
wrapGAppsHook,
pillow-jxl-plugin,
@@ -30,7 +30,7 @@ python3Packages.buildPythonApplication {
# Should be unnecessary once PR is pulled.
# PR: https://github.com/NixOS/nixpkgs/pull/271037
# Issue: https://github.com/NixOS/nixpkgs/issues/149812
wrapGAppsHook3
wrapGAppsHook
];
buildInputs = [
qt6.qtbase
@@ -70,7 +70,7 @@ python3Packages.buildPythonApplication {
"\${qtWrapperArgs[@]}"
];
pythonRemoveDeps = lib.optional (!withJXLSupport) "pillow_jxl";
pythonRemoveDeps = lib.optional (!withJXLSupport) [ "pillow_jxl" ];
pythonRelaxDeps = [
"numpy"
"pillow"
@@ -96,6 +96,7 @@ python3Packages.buildPythonApplication {
numpy
opencv-python
pillow
pillow-avif-plugin
pillow-heif
py7zr
pydantic

View File

@@ -51,7 +51,7 @@ let
# Should be unnecessary once PR is pulled.
# PR: https://github.com/NixOS/nixpkgs/pull/271037
# Issue: https://github.com/NixOS/nixpkgs/issues/149812
wrapGAppsHook3
wrapGAppsHook
];
buildInputs = with pkgs.qt6; [
qtbase
@@ -87,7 +87,7 @@ pkgs.mkShellNoCC {
env = {
QT_QPA_PLATFORM = "wayland;xcb";
UV_NO_SYNC = 1;
UV_NO_SYNC = "1";
UV_PYTHON_DOWNLOADS = "never";
};
@@ -111,8 +111,7 @@ pkgs.mkShellNoCC {
fi
source "''${venv}"/bin/activate
PYTHONPATH=${pythonPath}''${PYTHONPATH:+:''${PYTHONPATH}}
export PYTHONPATH
PYTHONPATH=${pythonPath}''${PYTHONPATH:+:}''${PYTHONPATH:-}
if [ ! -f "''${venv}"/pyproject.toml ] || ! diff --brief pyproject.toml "''${venv}"/pyproject.toml >/dev/null; then
printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2

View File

@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.5.6"
version = "9.5.5"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.13"
@@ -16,7 +16,8 @@ dependencies = [
"mutagen~=1.47",
"numpy~=2.2",
"opencv_python~=4.11",
"Pillow>=10.2,<12",
"Pillow>=10.2,<=11",
"pillow-avif-plugin~=1.5",
"pillow-heif~=0.22",
"pillow-jxl-plugin~=1.3",
"py7zr==1.0.0",

View File

@@ -2,7 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
VERSION: str = "9.5.6" # Major.Minor.Patch
VERSION: str = "9.5.5" # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
# The folder & file names where TagStudio keeps its data relative to a library.

View File

@@ -146,7 +146,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
def __separate_tags(
self, terms: list[AST], only_single: bool = True
) -> tuple[list[int], list[ColumnElement[bool]]]:
tag_ids: set[int] = set()
tag_ids: list[int] = []
bool_expressions: list[ColumnElement[bool]] = []
for term in terms:
@@ -154,7 +154,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
match term.type:
case ConstraintType.TagID:
try:
tag_ids.add(int(term.value))
tag_ids.append(int(term.value))
except ValueError:
logger.error(
"[SQLBoolExpressionBuilder] Could not cast value to an int Tag ID",
@@ -164,10 +164,10 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
case ConstraintType.Tag:
ids = self.__get_tag_ids(term.value)
if not only_single:
tag_ids.update(ids)
tag_ids.extend(ids)
continue
elif len(ids) == 1:
tag_ids.add(ids[0])
tag_ids.append(ids[0])
continue
case ConstraintType.FileType:
pass
@@ -179,7 +179,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
raise NotImplementedError(f"Unhandled constraint: '{term.type}'")
bool_expressions.append(self.visit(term))
return list(tag_ids), bool_expressions
return tag_ids, bool_expressions
def __entry_has_all_tags(self, tag_ids: list[int]) -> ColumnElement[bool]:
"""Returns Binary Expression that is true if the Entry has all provided tag ids."""

View File

@@ -17,7 +17,6 @@ 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.core.utils.silent_subprocess import silent_run # pyright: ignore
from tagstudio.core.utils.types import unwrap
logger = structlog.get_logger(__name__)
@@ -31,27 +30,24 @@ class RefreshTracker:
def files_count(self) -> int:
return len(self.files_not_in_library)
def save_new_files(self) -> Iterator[int]:
def save_new_files(self):
"""Save the list of files that are not in the library."""
batch_size = 200
index = 0
while index < len(self.files_not_in_library):
yield index
end = min(len(self.files_not_in_library), index + batch_size)
if self.files_not_in_library:
entries = [
Entry(
path=entry_path,
folder=unwrap(self.library.folder),
folder=self.library.folder, # pyright: ignore[reportArgumentType]
fields=[],
date_added=dt.now(),
)
for entry_path in self.files_not_in_library[index:end]
for entry_path in self.files_not_in_library
]
self.library.add_entries(entries)
index = end
self.files_not_in_library = []
yield
def refresh_dir(self, library_dir: Path, force_internal_tools: bool = False) -> Iterator[int]:
"""Scan a directory for files, and add those relative filenames to internal variables.

View File

@@ -10,7 +10,7 @@ from datetime import datetime as dt
from warnings import catch_warnings
import structlog
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import (
QFrame,
@@ -22,6 +22,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from tagstudio.core.enums import Theme
from tagstudio.core.library.alchemy.enums import FieldTypeEnum
from tagstudio.core.library.alchemy.fields import (
@@ -50,6 +51,9 @@ logger = structlog.get_logger(__name__)
class FieldContainers(QWidget):
"""The Preview Panel Widget."""
favorite_updated = Signal(bool)
archived_updated = Signal(bool)
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
@@ -127,7 +131,7 @@ class FieldContainers(QWidget):
container_index += 1
container_len += 1
if update_badges:
self.driver.emit_badge_signals({t.id for t in entry_tags})
self.emit_badge_signals({t.id for t in entry_tags})
# Write field container(s)
for index, field in enumerate(entry_fields, start=container_index):
@@ -238,7 +242,7 @@ class FieldContainers(QWidget):
self.driver.selected,
tag_ids=tags,
)
self.driver.emit_badge_signals(tags, emit_on_absent=False)
self.emit_badge_signals(tags, emit_on_absent=False)
def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
"""Update/Create data for a FieldContainer.
@@ -489,3 +493,16 @@ class FieldContainers(QWidget):
result = remove_mb.exec_()
if result == QMessageBox.ButtonRole.ActionRole.value:
callback()
def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):
"""Emit any connected signals for updating badge icons."""
logger.info("[emit_badge_signals] Emitting", tag_ids=tag_ids, emit_on_absent=emit_on_absent)
if TAG_ARCHIVED in tag_ids:
self.archived_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.archived_updated.emit(False) # noqa: FBT003
if TAG_FAVORITE in tag_ids:
self.favorite_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.favorite_updated.emit(False) # noqa: FBT003

View File

@@ -304,7 +304,6 @@ class TagSearchPanel(PanelWidget):
tag_widget.on_edit.disconnect()
tag_widget.on_remove.disconnect()
tag_widget.bg_button.clicked.disconnect()
tag_widget.search_for_tag_action.triggered.disconnect()
tag_id = tag.id
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))

View File

@@ -19,6 +19,7 @@ from xml.etree.ElementTree import Element
import cv2
import numpy as np
import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport]
import py7zr
import py7zr.io
import rarfile

View File

@@ -23,7 +23,7 @@ LANGUAGES = {
"French": "fr",
"German": "de",
"Hungarian": "hu",
"Italian": "it",
# "Italian": "it", # Minimal
"Japanese": "ja",
"Norwegian Bokmål": "nb_NO",
"Polish": "pl",

View File

@@ -177,9 +177,6 @@ class QtDriver(DriverMixin, QObject):
SIGTERM = Signal()
favorite_updated = Signal(bool)
archived_updated = Signal(bool)
tag_manager_panel: PanelModal | None = None
color_manager_panel: TagColorManager | None = None
ignore_modal: PanelModal | None = None
@@ -360,9 +357,8 @@ class QtDriver(DriverMixin, QObject):
self.tag_manager_panel = PanelModal(
widget=TagDatabasePanel(self, self.lib),
title=Translations["tag_manager.title"],
done_callback=lambda checked=False: (
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
),
done_callback=lambda checked=False,
s=self.selected: self.main_window.preview_panel.set_selection(s, update_preview=False),
has_save=False,
)
@@ -373,9 +369,9 @@ class QtDriver(DriverMixin, QObject):
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.tag_chosen.connect(
lambda chosen_tag: (
self.add_tags_to_selected_callback([chosen_tag]),
self.main_window.preview_panel.set_selection(self.selected),
lambda t, s=self.selected: (
self.add_tags_to_selected_callback(t),
self.main_window.preview_panel.set_selection(s),
)
)
@@ -563,12 +559,12 @@ class QtDriver(DriverMixin, QObject):
self.main_window.search_field.textChanged.connect(self.update_completions_list)
self.archived_updated.connect(
self.main_window.preview_panel.field_containers_widget.archived_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.ARCHIVED: hidden}, origin_id=0, add_tags=False
)
)
self.favorite_updated.connect(
self.main_window.preview_panel.field_containers_widget.favorite_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.FAVORITE: hidden}, origin_id=0, add_tags=False
)
@@ -804,19 +800,6 @@ class QtDriver(DriverMixin, QObject):
)
)
def emit_badge_signals(self, tag_ids: list[int] | set[int], emit_on_absent: bool = True):
"""Emit any connected signals for updating badge icons."""
logger.info("[emit_badge_signals] Emitting", tag_ids=tag_ids, emit_on_absent=emit_on_absent)
if TAG_ARCHIVED in tag_ids:
self.archived_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.archived_updated.emit(False) # noqa: FBT003
if TAG_FAVORITE in tag_ids:
self.favorite_updated.emit(True) # noqa: FBT003
elif emit_on_absent:
self.favorite_updated.emit(False) # noqa: FBT003
def add_tag_action_callback(self):
panel = BuildTagPanel(self.lib)
self.modal = PanelModal(
@@ -865,10 +848,9 @@ class QtDriver(DriverMixin, QObject):
self.main_window.preview_panel.set_selection(self.selected)
def add_tags_to_selected_callback(self, tag_ids: list[int]):
selected: list[int] = self.selected
selected = self.selected
self.main_window.thumb_layout.add_tags(selected, tag_ids)
self.lib.add_tags_to_entries(selected, tag_ids)
self.emit_badge_signals(tag_ids)
def delete_files_callback(self, origin_path: str | Path, origin_id: int | None = None):
"""Callback to send on or more files to the system trash.
@@ -1066,7 +1048,7 @@ class QtDriver(DriverMixin, QObject):
pw.show()
iterator.value.connect(
lambda _count: (
lambda: (
pw.update_label(
Translations.format(
"entries.running.dialog.new_entries", total=f"{files_count:n}"

View File

@@ -1,354 +1,13 @@
{
"about.config_path": "Percorso di Configurazione",
"about.description": "TagStudio è un'applicazione di organizzazione dei file e delle foto che utlizza un sistema di etichette focalizzato sul dare libertà e flessibilità all'utente. Niente programmi o formati proprietari, niente mare di file sidecar, e nessun stravolgimento della struttura del tuo sistema di file.",
"about.documentation": "Documentazione",
"about.license": "Licenza",
"about.module.found": "Trovato",
"about.title": "Informazioni su TagStudio",
"about.website": "Sito web",
"app.git": "Git Commit",
"app.pre_release": "Pre-rilascio",
"app.title": "{base_title} - Biblioteca '{library_dir}'",
"color.color_border": "Usa Colore Secondario per il Bordo",
"color.confirm_delete": "Sei sicuro di voler eliminare il colore \"{color_name}\"?",
"color.delete": "Elimina Etichetta",
"color.import_pack": "Importa Pacchetto di Colori",
"color.name": "Nome",
"color.namespace.delete.prompt": "Sei sicuro di voler eliminare questo spazio dei nomi dei colori? Questo eliminerà TUTTI i colori contenuti nello spazio dei nomi insieme ad esso!",
"color.namespace.delete.title": "Elimina Spazio dei Nomi dei Colori",
"color.new": "Nuovo Colore",
"color.placeholder": "Colore",
"color.primary": "Colore Primario",
"color.primary_required": "Colore Primario (Obbligatorio)",
"color.secondary": "Colore Secondario",
"color.title.no_color": "Nessun Colore",
"color_manager.title": "Gestisci Colori dei Tag",
"dependency.missing.title": "{dependency} Non Trovata",
"drop_import.description": "I seguenti file corrispondono a percorsi di file che già esistono nella biblioteca",
"drop_import.duplicates_choice.plural": "I seguenti {count} file corrispondono a percorsi di file già esistenti nella biblioteca.",
"drop_import.duplicates_choice.singular": "Il seguente file corrisponde ad un percorso di file già esistente nella biblioteca.",
"drop_import.progress.label.initial": "Importando Nuovi File...",
"drop_import.progress.label.plural": "Importando Nuovi File...\n{count} File Importati.{suffix}",
"drop_import.progress.label.singular": "Importando Nuovi File...\n1 File Importato.{suffix}",
"drop_import.progress.window_title": "Importa File",
"drop_import.title": "File in Conflitto",
"edit.color_manager": "Gestisci i Colori delle Etichette",
"edit.copy_fields": "Copia Campi",
"edit.paste_fields": "Incolla Campi",
"edit.tag_manager": "Gestisci Etichette",
"entries.duplicate.merge": "Unisci Voci Duplicate",
"entries.duplicate.merge.label": "Unendo Voci Duplicate...",
"entries.duplicate.refresh": "Ricarica Voci Duplicate",
"entries.duplicates.description": "Le voci duplicate sono definite come molteplici voci che puntano allo stesso file sul disco. Unendole verranno combinate le etichette ed i metadati da tutti i duplicati in una singola voce consolidata. Quese non devono essere confuse con i \"file duplicati\", che sono duplicati dei tuoi file stessi al di fuori di TagStudio.",
"entries.generic.refresh_alt": "&Ricarica",
"entries.generic.remove.removing": "Rimuovendo Voci",
"entries.generic.remove.removing_count": "Rimozione di {count} Voci...",
"entries.ignored.description": "Le voci dei file sono considerare \"ignorate\" se sono state aggiunte alla biblioteca prima che le regole di ignoranza dell'utente (attraverso il file '.ts_ignore') siano state aggiornate per escluderle. I file ignorati sono tenuti nella biblioteca, per impostazione predefinita, per prevenire perdite accidentali di dati durante l'aggiornamento delle regole di ignoranza.",
"entries.ignored.ignored_count": "Voci Ignorate: {count}",
"entries.ignored.remove": "Rimuovi Voci Ignorate",
"entries.ignored.remove_alt": "Rimuo&vi Voci Ignorate",
"entries.ignored.scanning": "Scansione della Biblioteca in cerca di Voci Ignorate...",
"entries.ignored.title": "Correggi Voci Ignorate",
"entries.mirror": "&Replica",
"entries.mirror.confirmation": "Sei sicuro di voler replicare le seguenti {count} voci?",
"entries.mirror.label": "Replicazione {idx}/{total} Voci...",
"entries.mirror.title": "Replicazione Voci",
"entries.mirror.window_title": "Replica Voci",
"entries.remove.plural.confirm": "Sei sicuro di voler rimuovere queste <b>{count}</b> voci dalla tua biblioteca? Nessun file su disco verrà eliminato.",
"entries.remove.singular.confirm": "Sei sicuro di voler rimuovere questa voce dalla tua biblioteca? Nessun file su disco verrà eliminato.",
"entries.running.dialog.new_entries": "Aggiundendo {total} Nuove Voci di File...",
"entries.running.dialog.title": "Aggiungendo Nuove Voci di File",
"entries.tags": "Etichette",
"entries.unlinked.description": "Ogni voce della biblioteca è collegata ad un file in una delle tue cartelle. Se un file collegato ad una voce viene spostato o eliminito al di fuori di TagStudio, la voce corrispondente viene considerata scollegata.<br><br>Le voci scollegate possono essere ricollegate automaticamente attraverso la ricerca nelle tue cartelle oppure cancellate se lo si desidera.",
"entries.unlinked.relink.attempting": "Tentativo di Ricollegare {index}/{unlinked_count} Voci, {fixed_count} Ricollegate con Successo",
"entries.unlinked.relink.manual": "Ricollegamento &Manuale",
"entries.unlinked.relink.title": "Ricollegamento Voci",
"entries.unlinked.remove": "Rimuovi Voci non Collegate",
"entries.unlinked.remove_alt": "Rimuo&vi Voci non Collegate",
"entries.unlinked.scanning": "Scansionando la Biblioteca in cerca di Voci non Collegate...",
"entries.unlinked.search_and_relink": "&Ricerca && Ricollega",
"entries.unlinked.title": "Correggi Voci non Collegate",
"entries.unlinked.unlinked_count": "Voci non Collegate: {count}",
"ffmpeg.missing.description": "FFmpeg e/o FFprobe non sono stati trovati. FFmpeg è necessario per la riproduzione multimediale e per le miniature.",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
"field.copy": "Copia Campo",
"field.edit": "Modifica Campo",
"field.paste": "Incolla Campo",
"file.date_added": "Data Aggiunta",
"file.date_created": "Data di Creazione",
"file.date_modified": "Data di Modifica",
"file.dimensions": "Dimensioni",
"file.duplicates.description": "TagStudio supporta l'importazione di risultati di DupeGuru per gestire file duplicati.",
"file.duplicates.dupeguru.advice": "Dopo la replicazione, sei libero di usare DupeGuru per eliminare i file indesiderati. Dopodiché, usa la funzione \"Correggi Voci non Collegate\" di TagStudio nel menu Strumenti per eliminare le voci non collegate.",
"file.duplicates.dupeguru.file_extension": "File di DupeGuru (*.dupeguru)",
"file.duplicates.dupeguru.load_file": "&Carica File di DupeGuru",
"file.duplicates.dupeguru.no_file": "Nessun File DupeGuru Selezionato",
"file.duplicates.dupeguru.open_file": "Apri il File dei Risulati di DupeGuru",
"file.duplicates.fix": "Corregi File Duplicati",
"file.duplicates.matches": "File Duplicati Corrispondenti: {count}",
"file.duplicates.matches_uninitialized": "File Duplicati Corrispondenti: N/A",
"file.duplicates.mirror_entries": "&Replica Voci",
"file.duration": "Lunghezza",
"file.not_found": "File Non Trovato",
"file.open_file": "Apri file",
"file.open_file_with": "Apri file con",
"file.open_location.generic": "Mostra file in File Explorer",
"file.open_location.mac": "Mostra nel Finder",
"file.open_location.windows": "Mostra in File Explorer",
"file.path": "Percorso del File",
"folders_to_tags.close_all": "Chiudi Tutto",
"folders_to_tags.converting": "Conversione delle cartelle in Etichette",
"folders_to_tags.description": "Crea Etichette basate sulla struttura delle tue cartelle e le applica alle tue voci.\nLa struttura riportata di seguito mostra tutte le etichette che verranno create ed a che voci verranno applicate.",
"folders_to_tags.open_all": "Apri Tutto",
"folders_to_tags.title": "Crea Etichette dalle Cartelle",
"generic.add": "Aggiungi",
"generic.apply": "Applica",
"generic.apply_alt": "&Applica",
"generic.cancel": "Annulla",
"generic.cancel_alt": "&Annulla",
"generic.close": "Chiudi",
"generic.continue": "Continua",
"generic.copy": "Copia",
"generic.cut": "Taglia",
"generic.delete": "Elimina",
"generic.delete_alt": "&Elimina",
"generic.done": "Fatto",
"generic.done_alt": "&Fatto",
"generic.edit": "Modifica",
"generic.edit_alt": "&Modifica",
"generic.filename": "Nome File",
"generic.missing": "Mancante",
"generic.navigation.back": "Indietro",
"generic.navigation.next": "Prossimo",
"generic.no": "No",
"generic.none": "Nessuno",
"generic.overwrite": "Sovrascrivi",
"generic.overwrite_alt": "&Sovrascrivi",
"generic.paste": "Incolla",
"generic.recent_libraries": "Biblioteche Recenti",
"generic.remove": "Rimuovi",
"generic.remove_alt": "&RImuovi",
"generic.rename": "Rinomina",
"generic.rename_alt": "&Rinomina",
"generic.reset": "Ripristina",
"generic.save": "Salva",
"generic.skip": "Salta",
"generic.skip_alt": "&Salta",
"generic.yes": "Sì",
"generic.recent_libraries": "Librerias Recenti",
"home.search": "Cerca",
"home.search_entries": "Cerca Voci",
"home.search_library": "Cerca Biblioteca",
"home.search_tags": "Cerca Etichette",
"home.thumbnail_size": "Dimensione Miniature",
"home.thumbnail_size.extra_large": "Miniature Molto Grandi",
"home.thumbnail_size.large": "Miniature Grandi",
"home.thumbnail_size.medium": "Miniature Medie",
"home.thumbnail_size.mini": "Miniature Mini",
"home.thumbnail_size.small": "Miniature Piccole",
"ignore.open_file": "Mostra File \"{ts_ignore}\" su Disco",
"json_migration.checking_for_parity": "Controllo della Parità...",
"json_migration.creating_database_tables": "Creazione Tabelle di Database SQL...",
"json_migration.description": "<br>Avvia e visualizza in anteprima i risultati del processo di migrazione della biblioteca. La biblioteca convertita <i>non</i> verrà utilizzata finché non clicchi su \"Completa Migrazione\". <br><br>I dati della biblioteca devono avere valori corrispondenti o presentare un'etichetta \"Corrispondente\". I valori che non corrispondono saranno visualizzati in rosso e contrassegnati con un simbolo \"<b>(!)</b>\" accanto a loro.<br><center><i>Questo processo può richiedere diversi minuti per biblioteche di grandi dimensioni.</i></center>",
"json_migration.discrepancies_found": "Discrepanze Riscontrate nella Biblioteca",
"json_migration.discrepancies_found.description": "Sono state riscontrate delle discrepanze tra i formati originali e quelli della biblioteca convertita. Si prega di verificare e scegliere se continuare con la migrazione oppure se annullarla.",
"json_migration.finish_migration": "Completa Migrazione",
"json_migration.heading.aliases": "Alias:",
"json_migration.heading.colors": "Colori:",
"json_migration.heading.differ": "Discrepanze",
"json_migration.heading.extension_list_type": "Tipo di Lista di Entensioni:",
"json_migration.heading.file_extension_list": "Elenco Estensioni dei File:",
"json_migration.heading.match": "Abbinato",
"json_migration.heading.names": "Nomi:",
"json_migration.heading.parent_tags": "Etichette Padre:",
"json_migration.heading.paths": "Percorsi:",
"json_migration.heading.shorthands": "Abbreviazioni:",
"json_migration.migrating_files_entries": "Migrando {entries:,d} Voci di File...",
"json_migration.migration_complete": "Migrazione Completata!",
"json_migration.migration_complete_with_discrepancies": "Migrazione Completata, Discrepanze Rilevate",
"json_migration.start_and_preview": "Avvio ed Anteprima",
"json_migration.title": "Salva Formato Migrazione: \"{path}\"",
"json_migration.title.new_lib": "<h2>v9.5+ Biblioteca</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Biblioteca</h2>",
"landing.open_create_library": "Apri/Crea Biblioteca {shortcut}",
"library.field.add": "Aggiungi Campo",
"library.field.confirm_remove": "Sei sicuro di voler rimuovere il campo \"{name}\"?",
"library.field.mixed_data": "Dati Misti",
"library.field.remove": "Rimuovi Campo",
"library.missing": "Manca la Posizione della Biblioteca",
"library.name": "Biblioteca",
"library.refresh.scanning.plural": "Ricerca di Nuovi File nelle Cartelle...\n{searched_count} Files Cercati, {found_count} Nuovi File Trovati",
"library.refresh.scanning.singular": "Ricerca di Nuovi File nelle Cartelle...\n{searched_count} File Cercati, {found_count} Nuovi File Trovati",
"library.refresh.scanning_preparing": "Ricerca di Nuovi File nelle Cartelle...\nPreparazione in corso...",
"library.refresh.title": "Aggiornamento delle Cartelle",
"library.scan_library.title": "Scansione della Biblioteca",
"library_info.cleanup": "Pulizia",
"library_info.cleanup.backups": "Backup della Biblioteca:",
"library_info.cleanup.dupe_files": "File Duplicati:",
"library_info.cleanup.ignored": "Voci Ignorate:",
"library_info.cleanup.unlinked": "Voci non Collegate:",
"library_info.stats": "Statistiche",
"library_info.stats.colors": "Colori Etichette:",
"library_info.stats.entries": "Voci:",
"library_info.stats.fields": "Campi:",
"library_info.stats.macros": "Macro:",
"library_info.stats.namespaces": "Spazi dei Nomi:",
"library_info.stats.tags": "Etichette:",
"library_info.title": "Biblioteca '{library_dir}'",
"library_info.version": "Versione del Formato della Biblioteca: {version}",
"library_object.name": "Nome",
"library_object.name_required": "Nome (Obbligatorio)",
"library_object.slug": "ID Slug",
"library_object.slug_required": "ID Slug (Obbligatorio)",
"macros.running.dialog.new_entries": "Esecuzione delle Macro Configurate su {count}/{total} Nuove Voci di File...",
"macros.running.dialog.title": "Esecuzione delle Macro sulle Nuove Voci",
"media_player.autoplay": "Riproduzione Automatica",
"media_player.loop": "Metti in Loop",
"menu.delete_selected_files_ambiguous": "Sposta File nel {trash_term}",
"menu.delete_selected_files_plural": "Sposta Files nel {trash_term}",
"menu.delete_selected_files_singular": "Sposta il File nel {trash_term}",
"menu.edit": "Modifica",
"menu.edit.ignore_files": "Ignora File e Cartelle",
"menu.edit.manage_tags": "Gestisci Etichette",
"menu.edit.new_tag": "Nuova &Etichetta",
"menu.file": "&File",
"menu.file.clear_recent_libraries": "Cancella Recenti",
"menu.file.close_library": "&Chiudi Biblioteca",
"menu.file.missing_library.message": "La posizione della biblioteca \"{library}\" non può essere trovata.",
"menu.file.missing_library.title": "Biblioteca Mancante",
"menu.file.new_library": "Nuova Biblioteca",
"menu.file.open_backups_folder": "Apri Cartella dei Backup",
"menu.file.open_create_library": "&Apri/Crea Biblioteca",
"menu.file.open_library": "Apri Biblioteca",
"menu.file.open_recent_library": "Apri Recenti",
"menu.file.refresh_directories": "&Aggiorna Cartelle",
"menu.file.save_backup": "&Salva Backup della Biblioteca",
"menu.file.save_library": "Salva Biblioteca",
"menu.help": "&Aiuto",
"menu.help.about": "Informazioni",
"menu.macros": "&Macro",
"menu.macros.folders_to_tags": "Cartelle ad Etichette",
"menu.select": "Seleziona",
"menu.settings": "Impostazioni...",
"menu.tools": "&Strumenti",
"menu.tools.fix_duplicate_files": "Coreggi File &Duplicati",
"menu.tools.fix_ignored_entries": "Correggi Voci &Ignorate",
"menu.tools.fix_unlinked_entries": "Correggi Voci &Scollegate",
"menu.view": "&Visualizza",
"menu.view.decrease_thumbnail_size": "Riduci Dimensione Miniature",
"menu.view.increase_thumbnail_size": "Aumenta Dimensione Miniature",
"menu.view.library_info": "&Informazioni Biblioteca",
"menu.file": "File",
"menu.window": "Finestra",
"namespace.create.description": "Gli spazi dei nomi sono usati da TagStudio per separare gruppi di elementi come etichette e colori in maniera da facilitarne l'esportazione e la condivisione. Gli spazi dei nomi che iniziano con \"tagstudio\" sono riservati per l'uso interno di TagStudio.",
"namespace.create.description_color": "I colori delle etichette usano gli spazi dei nomi come gruppi di tavolozze di colori. Tutti i colori personalizzati devono prima essere inseriti in uno spazio dei nomi.",
"namespace.create.title": "Crea Spazio dei Nomi",
"namespace.new.button": "Nuovo Spazio dei Nomi",
"namespace.new.prompt": "Crea un Nuovo Spazio dei Nomi per Iniziare ad Aggiungere Colori Personalizzati!",
"preview.ignored": "Ignorato",
"preview.multiple_selection": "<b>{count}</b> Elementi Selezionati",
"preview.no_selection": "Nessun Elemento Selezionato",
"preview.unlinked": "Scollegato",
"select.add_tag_to_selected": "Aggiungi Etichetta alla Selezione",
"select.all": "Seleziona Tutto",
"select.clear": "Cancella Selezione",
"select.inverse": "Inverti Selezione",
"settings.clear_thumb_cache.title": "Cancella Cache delle Miniature",
"settings.dateformat.english": "Inglese",
"settings.dateformat.international": "Internazionale",
"settings.dateformat.label": "Formato Data",
"settings.dateformat.system": "Sistema",
"settings.filepath.label": "Visibilità del Percorso del File",
"settings.filepath.option.full": "Mostra Percorsi Completi",
"settings.filepath.option.name": "Mostra Solo i Nomi dei File",
"settings.filepath.option.relative": "Mostra Percorsi Relativi",
"settings.generate_thumbs": "Generazione delle Miniature",
"settings.global": "Impostazioni Globali",
"settings.hourformat.label": "Formato a 24 Ore",
"settings.infinite_scroll": "Scorrimento Infinito",
"settings.language": "Lingua",
"settings.library": "Impostazioni Biblioteca",
"settings.open_library_on_start": "Apri Biblioteca all'Avvio",
"settings.page_size": "Dimensione Pagina",
"settings.restart_required": "Riavvia TagStudio affinchè le modifiche abbiano effetto.",
"settings.show_filenames_in_grid": "Mostra Nomi dei File nella Griglia",
"settings.show_recent_libraries": "Mostra Biblioteche Recenti",
"settings.splash.label": "Schermata Iniziale",
"settings.splash.option.classic": "Classico (9.0)",
"settings.splash.option.default": "Predefinito",
"settings.splash.option.goo_gears": "Open Source (9.4)",
"settings.splash.option.ninety_five": "'95 (9.5)",
"settings.splash.option.random": "Casuale",
"settings.tag_click_action.add_to_search": "Aggiungi Etichette alla Ricerca",
"settings.tag_click_action.label": "Azione di Clic sulle Etichette",
"settings.tag_click_action.open_edit": "Modifica Etichetta",
"settings.tag_click_action.set_search": "Cerca Etichetta",
"settings.theme.dark": "Scuro",
"settings.theme.label": "Tema:",
"settings.theme.light": "Chiaro",
"settings.theme.system": "Sistema",
"settings.thumb_cache_size.label": "Dimensione Cache delle Miniature",
"settings.title": "Impostazioni",
"sorting.direction.ascending": "Ascendente",
"sorting.direction.descending": "Discendente",
"sorting.mode.random": "Casuale",
"splash.opening_library": "Aprendo Biblioteca \"{library_path}\"...",
"status.deleted_file_plural": "{count} file eliminati!",
"status.deleted_file_singular": "1 file eliminato!",
"status.deleted_none": "Nessun file eliminato.",
"status.deleted_partial_warning": "Solo {count} file sono stati eliminati! Verifica se alcuni dei file sono attualmente mancanti o in uso.",
"status.deleting_file": "Eliminano file [{i}/{count}]: \"{path}\"...",
"status.library_backup_in_progress": "Salvataggio Backup della Biblioteca...",
"status.library_backup_success": "Backup della BIblioteca Salvato in: \"{path}\" ({time_span})",
"status.library_closed": "Biblioteca Chiusa ({time_span})",
"status.library_closing": "Chiudendo la Biblioteca...",
"status.library_save_success": "Biblioteca Salvata e Chiusa!",
"status.library_search_query": "Cercando nella Biblioteca...",
"status.library_version_expected": "Previsto:",
"status.library_version_found": "Trovato:",
"status.library_version_mismatch": "Versione della Biblioteca non Corrispondente!",
"status.results": "Risultati",
"status.results.invalid_syntax": "Sintassi di Ricerca Non Valida:",
"status.results_found": "{count} Risultati Trovati ({time_span})",
"tag.add": "Aggiungi Etichetta",
"tag.add.plural": "Aggiungi Etichette",
"tag.add_to_search": "Aggiungi alla Ricerca",
"tag.aliases": "Alias",
"tag.all_tags": "Tutte le Etichette",
"tag.choose_color": "Scegli Colore dell'Etichetta",
"tag.add": "Aggiungi Tag",
"tag.color": "Colore",
"tag.confirm_delete": "Sei sicuro di voler eliminare l'etichetta \"{tag_name}\"?",
"tag.create": "Crea Etichetta",
"tag.create_add": "Crea && Aggiungi \"{query}\"",
"tag.disambiguation.tooltip": "Usa questa etichetta per la disambiguazione",
"tag.edit": "Modifica Etichetta",
"tag.is_category": "È Categoria",
"tag.name": "Nome",
"tag.new": "Nuova Etichetta",
"tag.parent_tags.add": "Aggiungi Etichette Padre",
"tag.parent_tags.description": "Questa etichetta può essere considerata come sostitutiva di qualunque di queste Etichette Padre nelle richerche.",
"tag.remove": "Rimuovi Etichetta",
"tag.search_for_tag": "Cerca Etichetta",
"tag.shorthand": "Abbreviazione",
"tag.tag_name_required": "Nome Etichetta (Obbligatorio)",
"tag.view_limit": "Limite di Visualizzazione:",
"tag_manager.title": "Etichette della Biblioteca",
"trash.context.ambiguous": "Sposta file(s) in {trash_term}",
"trash.context.plural": "Sposta files in {trash_term}",
"trash.context.singular": "Sposta file in {trash_term}",
"trash.dialog.disambiguation_warning.plural": "Questo li rimuoverà da TagStudio <i>E</i> dal tuo sistema di file!",
"trash.dialog.disambiguation_warning.singular": "Questo lo rimuoverà da TagStudio <i>E</i> dal tuo sistema di file!",
"trash.dialog.move.confirmation.plural": "Sei sicuro di voler spostare questi {count} file nel {trash_term}?",
"trash.dialog.move.confirmation.singular": "Sei sicuro di voler spostare questo file nel {trash_term}?",
"trash.dialog.permanent_delete_warning": "<b>ATTENZIONE!</b> Se questo file non può essere spostato nel {trash_term}, verrà <b>eliminato permanentemente!</b>",
"trash.dialog.title.plural": "Elimina Files",
"trash.dialog.title.singular": "Elimina File",
"trash.name.generic": "Spazzatura",
"trash.name.windows": "Cestino",
"view.size.0": "Mini",
"view.size.1": "Piccolo",
"view.size.2": "Medio",
"view.size.3": "Grande",
"view.size.4": "Molto Grande",
"window.message.error_opening_library": "Errore nell'apertura della biblioteca.",
"window.title.error": "Errore",
"window.title.open_create_library": "Apri/Crea Biblioteca"
"tag.new": "Nuovo Tag"
}

View File

@@ -1,13 +1,12 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import SIGNAL
from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.mixed.tag_search import TagSearchPanel
from tagstudio.qt.mixed.tag_widget import TagWidget
from tagstudio.qt.ts_qt import QtDriver
def test_update_tags(qtbot: QtBot, library: Library):
@@ -18,35 +17,3 @@ def test_update_tags(qtbot: QtBot, library: Library):
# When
panel.update_tags()
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver, library: Library):
panel = TagSearchPanel(library)
qtbot.addWidget(panel)
panel.driver = qt_driver
# Set the widget
tags = library.tags
panel.set_tag_widget(tags[0], 0)
tag_widget: TagWidget = panel.scroll_layout.itemAt(0).widget()
should_replace_actions = {
tag_widget: ["on_edit()", "on_remove()"],
tag_widget.bg_button: ["clicked()"],
tag_widget.search_for_tag_action: ["triggered()"],
}
# Ensure each action has been set
ensure_one_receiver_per_action(should_replace_actions)
# Set the widget again
panel.set_tag_widget(tags[0], 0)
# Ensure each action has been replaced (amount of receivers is still 1)
ensure_one_receiver_per_action(should_replace_actions)
def ensure_one_receiver_per_action(should_replace_actions):
for action, signal_strings in should_replace_actions.items():
for signal_str in signal_strings:
assert action.receivers(SIGNAL(signal_str)) == 1