Compare commits

..

5 Commits

Author SHA1 Message Date
Travis Abendshien
cbb2d8d342 chore: bump version to v9.5.7 2026-05-05 16:12:49 -07:00
Travis Abendshien
735bf072b0 ci: bump macOS build targets to 14/15 2026-05-05 16:03:06 -07:00
TheBobBobs
6c8d800598 perf: bulk insert/delete tag_entries (#1296)
* perf: Bulk insert/delete tag_entries

* add type annotations
2026-04-29 01:30:57 -07:00
Juozas Skarbalius
695b3923c9 fix(ui): system theme fix (#1328)
* fix: introduce system theme fix in Mac os

* fix: formatting
2026-04-28 16:45:02 -07:00
Weblate (bot)
eacb93728b translations: update from Hosted Weblate (#1301)
* Translated using Weblate (German)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Patrick Schüle <patrickschuele90@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/de/
Translation: TagStudio/Strings

* Translated using Weblate (Italian)

Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Octavian <223219150+EdelFlosWeiss@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/it/
Translation: TagStudio/Strings

* Translated using Weblate (Dutch)

Currently translated at 37.5% (136 of 362 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 87.0% (315 of 362 strings)

Translated using Weblate (Finnish)

Currently translated at 87.0% (315 of 362 strings)

Translated using Weblate (Finnish)

Currently translated at 87.0% (315 of 362 strings)

Added translation using Weblate (Finnish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jonne Saloranta <saloranta.jonne@gmail.com>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fi/
Translation: TagStudio/Strings

* Translated using Weblate (Toki Pona)

Currently translated at 96.1% (348 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 96.1% (348 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 96.1% (348 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 96.1% (348 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 96.1% (348 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 95.0% (344 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 95.0% (344 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 95.0% (344 of 362 strings)

Translated using Weblate (Toki Pona)

Currently translated at 95.0% (344 of 362 strings)

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

---------

Co-authored-by: Patrick Schüle <patrickschuele90@gmail.com>
Co-authored-by: Octavian <223219150+EdelFlosWeiss@users.noreply.github.com>
Co-authored-by: Kristof Hermans <hermans1986@gmail.com>
Co-authored-by: Jonne Saloranta <saloranta.jonne@gmail.com>
Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>
Co-authored-by: Bee Crankson <ProfB.crankson@gmail.com>
Co-authored-by: Cyborus <cyborus@disroot.org>
2026-04-28 13:04:58 -07:00
7 changed files with 95 additions and 47 deletions

View File

@@ -48,17 +48,17 @@ jobs:
macos:
strategy:
matrix:
os-version: ['13', '14']
os-version: ['14', '15']
include:
- os-version: '13'
arch: x86_64
- os-version: '14'
arch: x86_64
- os-version: '15'
arch: aarch64
runs-on: macos-${{ matrix.os-version }}
env:
# INFO: Even though we run on 13, target towards compatibility
# INFO: Even though we run on 14, target towards compatibility
MACOSX_DEPLOYMENT_TARGET: '11.0'
steps:

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.7"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.13"

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.7" # 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

@@ -42,6 +42,7 @@ from sqlalchemy import (
text,
update,
)
from sqlalchemy.dialects import sqlite
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import (
InstanceState,
@@ -1478,7 +1479,7 @@ class Library:
return None
def add_tags_to_entries(
self, entry_ids: int | list[int] | set[int], tag_ids: int | list[int] | set[int]
self, entry_ids: int | Iterable[int], tag_ids: int | Iterable[int]
) -> int:
"""Add one or more tags to one or more entries.
@@ -1494,45 +1495,57 @@ class Library:
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else entry_ids
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
values: list[tuple[int, int]] = []
for tag_id in tag_ids_:
values.extend((tag_id, entry_id) for entry_id in entry_ids_)
with Session(self.engine, expire_on_commit=False) as session:
for tag_id in tag_ids_:
for entry_id in entry_ids_:
try:
session.add(TagEntry(tag_id=tag_id, entry_id=entry_id))
total_added += 1
session.commit()
except IntegrityError:
session.rollback()
for sub_list in [
values[i : i + MAX_SQL_VARIABLES // 2]
for i in range(0, len(values), MAX_SQL_VARIABLES // 2)
]:
stmt = (
sqlite.insert(TagEntry)
.values(sub_list)
.on_conflict_do_nothing()
.returning(TagEntry)
)
added = session.scalars(stmt).all()
total_added += len(added)
session.commit()
return total_added
def remove_tags_from_entries(
self, entry_ids: int | list[int] | set[int], tag_ids: int | list[int] | set[int]
) -> bool:
self, entry_ids: int | Iterable[int], tag_ids: int | Iterable[int]
):
"""Remove one or more tags from one or more entries."""
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else entry_ids
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else tag_ids
logger.info(
"[Library][remove_tags_from_entries]",
entry_ids=entry_ids,
tag_ids=tag_ids,
)
entry_ids_ = [entry_ids] if isinstance(entry_ids, int) else list(entry_ids)
tag_ids_ = [tag_ids] if isinstance(tag_ids, int) else list(tag_ids)
with Session(self.engine, expire_on_commit=False) as session:
try:
for tag_id in tag_ids_:
for entry_id in entry_ids_:
tag_entry = session.scalars(
select(TagEntry).where(
and_(
TagEntry.tag_id == tag_id,
TagEntry.entry_id == entry_id,
)
)
).first()
if tag_entry:
session.delete(tag_entry)
session.flush()
session.commit()
return True
except IntegrityError as e:
logger.error(e)
session.rollback()
return False
for tags_sub_list in [
tag_ids_[i : i + MAX_SQL_VARIABLES // 2]
for i in range(0, len(tag_ids_), MAX_SQL_VARIABLES // 2)
]:
for entries_sub_list in [
entry_ids_[i : i + MAX_SQL_VARIABLES // 2]
for i in range(0, len(entry_ids_), MAX_SQL_VARIABLES // 2)
]:
stmt = delete(TagEntry).where(
and_(
TagEntry.tag_id.in_(tags_sub_list),
TagEntry.entry_id.in_(entries_sub_list),
)
)
session.execute(stmt)
session.commit()
def add_color(self, color_group: TagColorGroup) -> TagColorGroup | None:
with Session(self.engine, expire_on_commit=False) as session:

View File

@@ -306,13 +306,13 @@ class QtDriver(DriverMixin, QObject):
sys.argv += ["-platform", "windows:darkmode=2"]
self.app = QApplication(sys.argv)
self.app.setStyle("Fusion")
if self.settings.theme == Theme.SYSTEM:
# TODO: detect theme instead of always setting dark
# Apply theme color if explicitly set to DARK or LIGHT by the user.
# For SYSTEM, we let Qt decide based on OS theme.
if self.settings.theme == Theme.DARK:
self.app.styleHints().setColorScheme(Qt.ColorScheme.Dark)
else:
self.app.styleHints().setColorScheme(
Qt.ColorScheme.Dark if self.settings.theme == Theme.DARK else Qt.ColorScheme.Light
)
elif self.settings.theme == Theme.LIGHT:
self.app.styleHints().setColorScheme(Qt.ColorScheme.Light)
if (
platform.system() == "Darwin" or platform.system() == "Windows"

View File

@@ -29,7 +29,7 @@
"drop_import.duplicates_choice.singular": "Seuraava tiedosto vastaa tiedostopolkua, joka on jo olemassa kirjastossa.",
"drop_import.progress.label.initial": "Tuo uusi tiedosto...",
"drop_import.progress.label.plural": "Tuodaan uusia tiedostoja...\n{count} Tiedostoa tuotu.{suffix}",
"drop_import.progress.label.singular": "Uusien tiedostojen tuonti...\n1 tiedosto tuotu",
"drop_import.progress.label.singular": "Uusien tiedostojen tuonti...\n1 tiedosto tuotu {suffix}",
"drop_import.progress.window_title": "Tuo tiedostoja",
"drop_import.title": "Ristiriitaisia tiedosto(ja)",
"edit.color_manager": "Hallitse tunnisteiden värejä",
@@ -54,7 +54,7 @@
"entries.mirror.label": "Peilataan {idx}/{total} merkintää...",
"entries.mirror.title": "Peilataan merkintöjä",
"entries.mirror.window_title": "Peilaa merkinnät",
"entries.remove.plural.confirm": "Haluatko varmasti poistaa nämä <b>{count</b> merkintää kirjastostasi? Levyllä olevia tiedostoja ei poisteta.",
"entries.remove.plural.confirm": "Haluatko varmasti poistaa nämä <b>{count}</b> merkintää kirjastostasi? Levyllä olevia tiedostoja ei poisteta.",
"entries.remove.singular.confirm": "Haluatko varmasti poistaa tämän merkinnän kirjastostasi? Levyllä olevia tiedostoja ei poisteta.",
"entries.running.dialog.new_entries": "Lisätään {total} uutta tiedosto merkintää...",
"entries.running.dialog.title": "Lisätään uudet tiedosto merkinnät",

View File

@@ -0,0 +1,35 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
"""Test theme handling in QtDriver, particularly the SYSTEM theme fix (issue #999)."""
from unittest.mock import Mock
import pytest
from PySide6.QtCore import Qt
from tagstudio.qt.global_settings import Theme
@pytest.mark.parametrize(
"theme,expected_call",
[
(Theme.DARK, Qt.ColorScheme.Dark),
(Theme.LIGHT, Qt.ColorScheme.Light),
(Theme.SYSTEM, None), # SYSTEM theme should NOT call setColorScheme
],
)
def test_theme_colorscheme_handling(theme: Theme, expected_call):
mock_style_hints = Mock()
if theme == Theme.DARK:
mock_style_hints.setColorScheme(Qt.ColorScheme.Dark)
elif theme == Theme.LIGHT:
mock_style_hints.setColorScheme(Qt.ColorScheme.Light)
if expected_call is None:
# SYSTEM theme should NOT call setColorScheme
mock_style_hints.setColorScheme.assert_not_called()
else:
mock_style_hints.setColorScheme.assert_called_once_with(expected_call)