Compare commits

..

6 Commits

Author SHA1 Message Date
Jann Stute
4dc06835cb fix: prevent deadlock when wanted mnemonics conflict (#1200)
* fix: prevent deadlock when wanted mnemonics conflict

* fix: remove invalid mnemonics from translations
2026-01-22 21:55:03 -08:00
Weblate (bot)
2a2d279725 translations: update from Hosted Weblate (#1266)
* Translated using Weblate (Thai)

Currently translated at 1.6% (6 of 362 strings)

Added translation using Weblate (Thai)

Co-authored-by: Anucha Hlownonkor <tony.chompoo@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/th/
Translation: TagStudio/Strings

* Translated using Weblate (Tamil)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ta/
Translation: TagStudio/Strings

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 87.2% (316 of 362 strings)

Co-authored-by: Asmodeus <colligare1Asmodeum@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/pt_BR/
Translation: TagStudio/Strings

* Translated using Weblate (Greek)

Currently translated at 44.7% (162 of 362 strings)

Translated using Weblate (Greek)

Currently translated at 44.7% (162 of 362 strings)

Translated using Weblate (Greek)

Currently translated at 27.0% (98 of 362 strings)

Added translation using Weblate (Greek)

Co-authored-by: Gvol <gvol@ncshosting.org>
Co-authored-by: Gvol <gvol@nexusystems.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/el/
Translation: TagStudio/Strings

* Translated using Weblate (Japanese)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ja/
Translation: TagStudio/Strings

* Translated using Weblate (Icelandic)

Currently translated at 11.0% (40 of 362 strings)

Added translation using Weblate (Icelandic)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kristinn Snær <mortallighting@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/is/
Translation: TagStudio/Strings

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Strings

* Translated using Weblate (Italian)

Currently translated at 100.0% (362 of 362 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (359 of 359 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Omni <omnipresentw@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/it/
Translation: TagStudio/Strings

* Translated using Weblate (Dutch)

Currently translated at 35.9% (130 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Typfout <timo.pollarini@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/nl/
Translation: TagStudio/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (362 of 362 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (359 of 359 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julen Arratibel Etxabe <jarratibeletxabe@gmail.com>
Co-authored-by: r40s-0 <andre.orenday@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Strings

* Translated using Weblate (French)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings

* Translated using Weblate (Cebuano)

Currently translated at 30.3% (109 of 359 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: StartsMercury <startsmercury@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ceb/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 94.4% (342 of 362 strings)

Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/tok/
Translation: TagStudio/Strings

---------

Co-authored-by: Anucha Hlownonkor <tony.chompoo@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: Asmodeus <colligare1Asmodeum@gmail.com>
Co-authored-by: Gvol <gvol@ncshosting.org>
Co-authored-by: Gvol <gvol@nexusystems.org>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: wany-oh <wany-oh@users.noreply.hosted.weblate.org>
Co-authored-by: Kristinn Snær <mortallighting@gmail.com>
Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Co-authored-by: Omni <omnipresentw@users.noreply.hosted.weblate.org>
Co-authored-by: Typfout <timo.pollarini@gmail.com>
Co-authored-by: Julen Arratibel Etxabe <jarratibeletxabe@gmail.com>
Co-authored-by: r40s-0 <andre.orenday@gmail.com>
Co-authored-by: Med <45147847+kitsumed@users.noreply.github.com>
Co-authored-by: StartsMercury <startsmercury@gmail.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
2026-01-22 21:46:31 -08:00
Jann Stute
32a9a04399 fix: tab order in build_tag modal (#1235) 2026-01-21 23:50:10 -08:00
Jann Stute
785959ec24 fix: pyright errors in blender_renderer.py (#1236) 2026-01-21 22:47:29 -08:00
Jann Stute
97c9d34186 fix: errors in DupeFilesRegistry (#1233) 2026-01-21 22:22:16 -08:00
TheBobBobs
57849bf4d5 remove entry even if deleting it's file failed (#1246) 2026-01-21 18:15:01 -08:00
18 changed files with 89 additions and 71 deletions

View File

@@ -7,6 +7,7 @@ import structlog
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap
logger = structlog.get_logger()
@@ -28,7 +29,7 @@ class DupeFilesRegistry:
A duplicate file is defined as an identical or near-identical file as determined
by a DupeGuru results file.
"""
library_dir = self.library.library_dir
library_dir = unwrap(self.library.library_dir)
if not isinstance(results_filepath, Path):
results_filepath = Path(results_filepath)
@@ -43,7 +44,7 @@ class DupeFilesRegistry:
files: list[Entry] = []
for element in group:
if element.tag == "file":
file_path = Path(element.attrib.get("path"))
file_path = Path(unwrap(element.attrib.get("path")))
try:
path_relative = file_path.relative_to(library_dir)
@@ -82,5 +83,5 @@ class DupeFilesRegistry:
for i, entries in enumerate(self.groups):
remove_ids = entries[1:]
logger.info("Removing entries group", ids=remove_ids)
self.library.remove_entries(remove_ids)
self.library.remove_entries([e.id for e in remove_ids])
yield i - 1 # The -1 waits for the next step to finish

View File

@@ -2,8 +2,6 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
# pyright: reportExplicitAny=false
import os
import subprocess
import sys

View File

@@ -522,7 +522,7 @@ class BuildTagPanel(PanelWidget):
self.alias_names.clear()
last: QWidget = self.panel_save_button
last: QWidget | None = self.panel_save_button
for alias_id in self.alias_ids:
alias = self.lib.get_alias(self.tag.id, alias_id)
@@ -549,7 +549,8 @@ class BuildTagPanel(PanelWidget):
self.aliases_table.setCellWidget(row, 1, new_item)
self.aliases_table.setCellWidget(row, 0, remove_btn)
self.setTabOrder(last, self.aliases_table.cellWidget(row, 1))
if last is not None:
self.setTabOrder(last, self.aliases_table.cellWidget(row, 1))
self.setTabOrder(
self.aliases_table.cellWidget(row, 1), self.aliases_table.cellWidget(row, 0)
)
@@ -624,3 +625,4 @@ class BuildTagPanel(PanelWidget):
self.setTabOrder(unwrap(self.panel_save_button), self.aliases_table.cellWidget(0, 1))
self.name_field.selectAll()
self.name_field.setFocus()
self._set_aliases()

View File

@@ -1,10 +1,13 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import re
import structlog
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QMenu
logger = structlog.get_logger(__name__)
def remove_mnemonic_marker(label: str) -> str:
"""Remove existing accelerator markers (&) from a label."""
@@ -25,6 +28,31 @@ def remove_mnemonic_marker(label: str) -> str:
return result
def get_wanted_mnemonics(text: str) -> list[str]:
matches = re.findall("(?:^|[^&])&([^&])", text)
return matches
def sanitise_mnemonics(actions: list[QAction]) -> None:
previous = []
for action in actions:
text = action.text()
m = get_wanted_mnemonics(text)
if len(m) == 0:
continue
elif len(m) > 1:
logger.warning("Found multiple wanted mnemonics, removing all", text=text)
action.setText(remove_mnemonic_marker(text))
continue
elif m[0] in previous:
logger.warning("Removing conflicting mnemonic", text=text)
action.setText(remove_mnemonic_marker(text))
continue
previous.append(m[0])
# Additional weight for first character in string
FIRST_CHARACTER_EXTRA_WEIGHT = 50
# Additional weight for the beginning of a word
@@ -97,6 +125,9 @@ def assign_mnemonics(menu: QMenu):
# Collect actions
actions = [a for a in menu.actions() if not a.isSeparator()]
# sanitise mnemonics to prevent deadlocks
sanitise_mnemonics(actions)
# Sequence map: mnemonic key -> QAction
sequence_to_action: dict[str, QAction] = {}

View File

@@ -790,25 +790,17 @@ class ThumbRenderer(QObject):
)
im: Image.Image | None = None
try:
blend_image = blend_thumb(str(filepath))
bg = Image.new("RGB", blend_image.size, color=bg_color)
bg.paste(blend_image, mask=blend_image.getchannel(3))
im = bg
except (
AttributeError,
UnidentifiedImageError,
TypeError,
) as e:
if str(e) == "expected string or buffer":
if (blend_image := blend_thumb(str(filepath))) is not None:
bg = Image.new("RGB", blend_image.size, color=bg_color)
bg.paste(blend_image, mask=blend_image.getchannel(3))
im = bg
else:
logger.info(
f"[ThumbRenderer][BLENDER][INFO] {filepath.name} "
f"Doesn't have an embedded thumbnail. ({type(e).__name__})"
"Doesn't have an embedded thumbnail."
)
else:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
except Exception as e:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
return im
@staticmethod

View File

@@ -32,7 +32,7 @@ from io import BufferedReader
from PIL import Image, ImageOps
def blend_extract_thumb(path):
def blend_extract_thumb(path) -> tuple[bytes | None, int, int]:
rend = b"REND"
test = b"TEST"
@@ -97,8 +97,10 @@ def blend_extract_thumb(path):
return image_buffer, x, y
def blend_thumb(file_in):
def blend_thumb(file_in) -> Image.Image | None:
buf, width, height = blend_extract_thumb(file_in)
if buf is None:
return None
image = Image.frombuffer(
"RGBA",
(width, height),

View File

@@ -383,7 +383,7 @@ class ThumbGridLayout(QLayout):
@override
def itemAt(self, index: int) -> QLayoutItem:
if index >= len(self._items):
return None
return None # pyright: ignore[reportReturnType]
return self._items[index]
@override

View File

@@ -899,6 +899,7 @@ class QtDriver(DriverMixin, QObject):
deleted_count: int = 0
selected = self.selected
library_dir = unwrap(self.lib.library_dir)
if len(selected) <= 1 and origin_path:
origin_id_ = origin_id
@@ -907,7 +908,7 @@ class QtDriver(DriverMixin, QObject):
origin_id_ = selected[0]
pending.append((origin_id_, Path(origin_path)))
elif (len(selected) > 1) or (len(selected) <= 1):
else:
for item in selected:
entry = self.lib.get_entry(item)
filepath: Path = entry.path
@@ -924,39 +925,30 @@ class QtDriver(DriverMixin, QObject):
e_id, f = tup
if (origin_path == f) or (not origin_path):
self.main_window.preview_panel.preview_thumb.media_player.stop()
if delete_file(self.lib.library_dir / f):
self.main_window.status_bar.showMessage(
Translations.format(
"status.deleting_file", i=i, count=len(pending), path=f
)
)
self.main_window.status_bar.repaint()
self.lib.remove_entries([e_id])
msg = Translations.format(
"status.deleting_file", i=i, count=len(pending), path=f
)
self.main_window.status_bar.showMessage(msg)
self.main_window.status_bar.repaint()
self.lib.remove_entries([e_id])
if delete_file(library_dir / f):
deleted_count += 1
selected.clear()
self.clear_select_action_callback()
if deleted_count > 0:
self.update_browsing_state()
self.main_window.preview_panel.set_selection(selected)
self.clear_select_action_callback()
self.update_browsing_state()
if len(selected) <= 1 and deleted_count == 0:
self.main_window.status_bar.showMessage(Translations["status.deleted_none"])
elif len(selected) <= 1 and deleted_count == 1:
self.main_window.status_bar.showMessage(
Translations.format("status.deleted_file_plural", count=deleted_count)
)
elif len(selected) > 1 and deleted_count == 0:
self.main_window.status_bar.showMessage(Translations["status.deleted_none"])
elif len(selected) > 1 and deleted_count < len(selected):
self.main_window.status_bar.showMessage(
Translations.format("status.deleted_partial_warning", count=deleted_count)
)
elif len(selected) > 1 and deleted_count == len(selected):
self.main_window.status_bar.showMessage(
Translations.format("status.deleted_file_plural", count=deleted_count)
)
if deleted_count > 0 and deleted_count != len(pending):
msg = Translations.format("status.deleted_partial_warning", count=deleted_count)
else:
index = min(deleted_count, 2)
msg = (
Translations["status.deleted_none"],
Translations["status.deleted_file_singular"],
Translations.format("status.deleted_file_plural", count=deleted_count),
)[index]
self.main_window.status_bar.showMessage(msg)
self.main_window.status_bar.repaint()
def delete_file_confirmation(self, count: int, filename: Path | None = None) -> int:

View File

@@ -224,7 +224,7 @@
"menu.file.open_create_library": "&Abrir/Crear biblioteca",
"menu.file.open_library": "Abrir biblioteca",
"menu.file.open_recent_library": "Abrir reciente",
"menu.file.refresh_directories": "&Actualizar directorios",
"menu.file.refresh_directories": "Actualizar directorios",
"menu.file.save_backup": "&Guardar copia de seguridad de la biblioteca",
"menu.file.save_library": "Guardar biblioteca",
"menu.help": "&Ayuda",

View File

@@ -33,7 +33,7 @@
"drop_import.progress.window_title": "Fájlok importálása",
"drop_import.title": "Fájlütközés",
"edit.color_manager": "&Színek kezelése",
"edit.copy_fields": "Mezők &másolása",
"edit.copy_fields": "Mezők másolása",
"edit.paste_fields": "Mezők &beillesztése",
"edit.tag_manager": "Címkék kezelése",
"entries.duplicate.merge": "Egyező elemek &egyesítése",
@@ -212,7 +212,7 @@
"menu.delete_selected_files_singular": "Fájl {trash_term} &helyezése",
"menu.edit": "S&zerkesztés",
"menu.edit.ignore_files": "Fájlok és mappák figyelmen kívül hagyása",
"menu.edit.manage_tags": "&Címkék ke&zelése",
"menu.edit.manage_tags": "Címkék kezelése",
"menu.edit.new_tag": "Ú&j címke",
"menu.file": "&Fájl",
"menu.file.clear_recent_libraries": "&Legutóbbi könyvtárak listájának törlése",

View File

@@ -224,7 +224,7 @@
"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.refresh_directories": "Aggiorna Cartelle",
"menu.file.save_backup": "&Salva Backup della Biblioteca",
"menu.file.save_library": "Salva Biblioteca",
"menu.help": "&Aiuto",

View File

@@ -195,7 +195,7 @@
"menu.edit.new_tag": "Ny &Etikett",
"menu.file": "Fil",
"menu.file.clear_recent_libraries": "Fjern Nylige",
"menu.file.close_library": "&Lukk Bibliotek",
"menu.file.close_library": "Lukk Bibliotek",
"menu.file.missing_library.message": "Plasseringen til biblioteket \"{library}\" kan ikke finnes.",
"menu.file.missing_library.title": "Manglende Bibliotek",
"menu.file.new_library": "Nytt Bibliotek",
@@ -212,7 +212,7 @@
"menu.select": "Velg",
"menu.settings": "Innstillinger...",
"menu.tools": "Verktøy",
"menu.tools.fix_duplicate_files": "Fiks Duplikate &Filer",
"menu.tools.fix_duplicate_files": "Fiks Duplikate Filer",
"menu.tools.fix_unlinked_entries": "Fiks &Frakoblede Oppføringer",
"menu.view": "&Se",
"menu.window": "Vindu",

View File

@@ -181,14 +181,14 @@
"menu.edit.new_tag": "Nowy &Tag",
"menu.file": "&Plik",
"menu.file.clear_recent_libraries": "Wyczyść ostatnie",
"menu.file.close_library": "&Zamknij bibliotekę",
"menu.file.close_library": "Zamknij bibliotekę",
"menu.file.missing_library.message": "Lokalizacja biblioteki \"{library}\" nie została odnaleziona.",
"menu.file.missing_library.title": "Brakująca biblioteka",
"menu.file.new_library": "Nowa biblioteka",
"menu.file.open_create_library": "&Otwórz/Stwórz bibliotekę",
"menu.file.open_library": "Otwórz bibliotekę",
"menu.file.open_recent_library": "Otwórz ostatnie",
"menu.file.refresh_directories": "&Odśwież katalogi",
"menu.file.refresh_directories": "Odśwież katalogi",
"menu.file.save_backup": "&Zapisz kopię zapasową biblioteki",
"menu.file.save_library": "Zapisz bibliotekę",
"menu.help": "&Pomoc",

View File

@@ -181,7 +181,7 @@
"menu.file.open_create_library": "&Abrir/Criar Biblioteca",
"menu.file.open_library": "Abrir Biblioteca",
"menu.file.open_recent_library": "Abrir Recente",
"menu.file.refresh_directories": "&Atualizar Pastas",
"menu.file.refresh_directories": "Atualizar Pastas",
"menu.file.save_backup": "&Gravar Backup da Biblioteca",
"menu.file.save_library": "Gravar Biblioteca",
"menu.help": "&Ajuda",

View File

@@ -224,7 +224,7 @@
"menu.file.open_create_library": "&Abrir/Criar Biblioteca",
"menu.file.open_library": "Abrir Biblioteca",
"menu.file.open_recent_library": "Abrir Recente",
"menu.file.refresh_directories": "&Atualizar Pastas",
"menu.file.refresh_directories": "Atualizar Pastas",
"menu.file.save_backup": "&Salvar Backup da Biblioteca",
"menu.file.save_library": "Salvar Biblioteca",
"menu.help": "&Ajuda",

View File

@@ -195,7 +195,7 @@
"menu.file.open_create_library": "&Открыть/создать библиотеку",
"menu.file.open_library": "Открыть библиотеку",
"menu.file.open_recent_library": "Открыть последнюю",
"menu.file.refresh_directories": "&Обновить папки",
"menu.file.refresh_directories": "Обновить папки",
"menu.file.save_backup": "&Сохранить резервную копию библиотеки",
"menu.file.save_library": "Сохранить библиотеку",
"menu.help": "&Помощь",

View File

@@ -216,7 +216,7 @@
"menu.edit.new_tag": "புதிய & குறிச்சொல்",
"menu.file": "கோப்பு (&f)",
"menu.file.clear_recent_libraries": "சமீபத்தியதை அழிக்கவும்",
"menu.file.close_library": "& நூலகம் மூடு",
"menu.file.close_library": " நூலகம் மூடு",
"menu.file.missing_library.message": "\"{library}\" நூலகத்தின் இருப்பிடத்தைக் கண்டுபிடிக்க முடியாது.",
"menu.file.missing_library.title": "நூலகம் இல்லை",
"menu.file.new_library": "புதிய நூலகம்",
@@ -225,7 +225,7 @@
"menu.file.open_library": "திறந்த நூலகம்",
"menu.file.open_recent_library": "அண்மைக் கால திறப்பு",
"menu.file.refresh_directories": "கோப்பகத்தை புதுப்பிக்கவும்",
"menu.file.save_backup": "& நூலக காப்புப்பிரதியை சேமிக்கவும்",
"menu.file.save_backup": " நூலக காப்புப்பிரதியை சேமிக்கவும்",
"menu.file.save_library": "நூலகத்தை சேமிக்கவும்",
"menu.help": "உதவி (&h)",
"menu.help.about": "பற்றி",

View File

@@ -182,7 +182,7 @@
"menu.edit.new_tag": "Yeni &Etiket",
"menu.file": "&Dosya",
"menu.file.clear_recent_libraries": "Yakın Geçmişi Temizle",
"menu.file.close_library": "Kütüphaneyi &Kapat",
"menu.file.close_library": "Kütüphaneyi Kapat",
"menu.file.new_library": "Yeni Kütüphane",
"menu.file.open_create_library": "Kütüphane &Aç/Oluştur",
"menu.file.open_library": "Kütüphane Aç",