Compare commits

..

10 Commits

Author SHA1 Message Date
Hosted Weblate 5efd153908 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 79.3% (335 of 422 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ??? <2807603108@qq.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/zh_Hans/
Translation: TagStudio/Application Strings
2026-07-21 15:01:26 +02:00
Hosted Weblate e106e9759c Translated using Weblate (French)
Currently translated at 100.0% (422 of 422 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/Application Strings
2026-07-21 15:01:25 +02:00
Hosted Weblate 5c282e0ada Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Co-authored-by: 2004milenadiaz-source <2004.milena.diaz@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julen Arratibel Etxabe <jarratibeletxabe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/es/
Translation: TagStudio/Application Strings
2026-07-21 15:01:25 +02:00
Hosted Weblate de1d8e5c1b Translated using Weblate (Hungarian)
Currently translated at 100.0% (423 of 423 strings)

Co-authored-by: Szíjártó Levente Pál <szijartoleventepal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/hu/
Translation: TagStudio/Application Strings
2026-07-21 15:01:24 +02:00
Travis Abendshien c3b6bec2ff chore: pass ripgrep label text to constructor 2026-07-19 19:49:37 -07:00
Travis Abendshien 58496a7d2d build: add nightly icons and strings 2026-07-19 19:48:27 -07:00
Travis Abendshien dc4251ff55 chore: bump version to v9.6.2 2026-07-19 19:19:29 -07:00
purpletennisball 174262b9b3 feat(thumbs): render .ai thumbnails (#1453) 2026-07-19 19:16:39 -07:00
Xarvex 27d761731c fix(ci): setup Ruff once, invoke manually
The parallel steps can have a race condition if they both have a lock on
the final Ruff installation path.

Initially, the action was reused for formatting and linting as the
ruff-action sets the necessary environment variables and matchers,
responsible for the annotations on the GitHub UI. Though, as it turns
out, this is already done whenever Ruff is installed, there is no
"special magic" whenever execution of the process happens.

So, only use the action for setup once, and in the parallel steps we can
invoke Ruff ourselves.
2026-07-19 16:11:40 -05:00
Jann Stute 51a9c16f50 refactor: remove dead folders table (#1444)
* refactor: remove dead folders table

* fix: remove folder param from all uses of Entry constructor

* fix: missing filename column after migration

* fix: bump db version to 300, since it is a breaking change

* fix: add empty library fixture for version 202
2026-07-17 17:35:08 -07:00
30 changed files with 156 additions and 371 deletions
+11 -4
View File
@@ -210,11 +210,18 @@ jobs:
steps:
- *checkout
- name: Setup Ruff
uses: astral-sh/ruff-action@v4.0.0
with:
# No-op operation, since executing Ruff cannot be disabled for the action.
# Note that `--version` has different behavior than `version`, as the
# latter will fail if passed any extra args, even if that arg is empty.
args: --version
src: ''
- parallel:
- name: Run Ruff linter
uses: astral-sh/ruff-action@v4.0.0
run: ruff check
- name: Run Ruff formatter
uses: astral-sh/ruff-action@v4.0.0
with:
args: format --check
run: ruff format --check
+1 -1
View File
@@ -8,7 +8,7 @@ path = [
"docs/CNAME",
"docs/assets/**",
"src/tagstudio/qt/resources.json",
"src/tagstudio/resources/icon.*",
"src/tagstudio/resources/icon*.*",
"src/tagstudio/resources/tagstudio.desktop",
"src/tagstudio/resources/templates/ts_ignore_template.txt",
"src/tagstudio/resources/templates/ts_ignore_template_blank.txt",
+1 -1
View File
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
[project]
name = "TagStudio"
description = "A User-Focused Photo & File Management System."
version = "9.6.1"
version = "9.6.2"
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.12,<3.14"
+2 -2
View File
@@ -3,8 +3,8 @@
from importlib.metadata import version
VERSION: str = version("tagstudio") # Major.Minor.Patch
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
VERSION: str = version("tagstudio")
BUILD_TYPE: str = "" # Usually "", "app.nightly", or "app.pre_release"
COPYRIGHT_YEARS: str = "2021-2026"
COPYRIGHT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien & TagStudio Contributors"
COPYRIGHT_COMPACT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien\n& TagStudio Contributors"
@@ -9,7 +9,7 @@ JSON_FILENAME: str = "ts_library.json"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 202
DB_VERSION: int = 300
TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
+44 -117
View File
@@ -7,7 +7,6 @@
# pyright: reportDeprecated=false
import platform
import re
import shutil
import sys
@@ -19,7 +18,6 @@ from datetime import UTC, datetime
from os import makedirs
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4
import sqlalchemy
import structlog
@@ -94,10 +92,8 @@ from tagstudio.core.library.alchemy.fields import (
TextFieldTemplate,
)
from tagstudio.core.library.alchemy.joins import TagEntry, TagParent
from tagstudio.core.library.alchemy.metadata import FileMetadata
from tagstudio.core.library.alchemy.models import (
Entry,
Folder,
Namespace,
Tag,
TagAlias,
@@ -107,7 +103,6 @@ from tagstudio.core.library.alchemy.models import (
from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder
from tagstudio.core.library.ignore import migrate_ext_list
from tagstudio.core.library.json.library import Library as JsonLibrary
from tagstudio.core.utils.stat import get_date_created, get_date_modified
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.translations import Translations
@@ -237,7 +232,6 @@ class Library:
library_dir: Path | None = None
engine: Engine | None = None
folder: Folder | None = None
included_files: set[Path] = set()
def __init__(self) -> None:
@@ -262,7 +256,6 @@ class Library:
"""Migrate JSON library data to the SQLite database."""
logger.info("Starting Library Conversion...")
start_time = time.time()
folder: Folder = Folder(path=self.library_dir, uuid=str(uuid4()))
# Tags
for tag in json_lib.tags:
@@ -315,7 +308,6 @@ class Library:
[
Entry(
path=entry.path / entry.filename,
folder=folder,
fields=[],
id=entry.id + 1, # NOTE: JSON IDs start at 0 instead of 1
date_added=datetime.now(),
@@ -482,16 +474,6 @@ class Library:
session.add(Version(key=DB_VERSION_CURRENT_KEY, value=DB_VERSION))
session.flush()
# add folder for current path
folder = Folder(
path=library_dir,
uuid=str(uuid4()),
)
session.add(folder)
session.expunge(folder)
session.flush()
self.folder = folder
# Generate default .ts_ignore file
try:
ts_ignore_template = (
@@ -587,6 +569,7 @@ class Library:
(self.__apply_db200_migration, 200, None), # changes: field tables
(self.__apply_db201_migration, 201, 200), # changes: field tables
(self.__apply_db202_migration, 202, None), # changes: tag_parents
(self.__apply_db300_migration, 300, None), # changes: deletes folders
]
for migration, v, iv in migrations:
if loaded_db_version < v and (iv is None or initial_db_version < iv):
@@ -604,22 +587,6 @@ class Library:
)
logger.info(f"[Library] Library migrated to DB version {DB_VERSION}")
with Session(self.engine) as session:
# TODO: the folder logic has no use and was never finished, remove it
# check if folder matching current path exists already
# NOTE: this has been causing new Folders to be created when the library is moved, since
# its introduction
self.folder = session.scalar(select(Folder).where(Folder.path == library_dir))
if not self.folder:
folder = Folder(
path=library_dir,
uuid=str(uuid4()),
)
session.add(folder)
session.expunge(folder)
session.commit()
self.folder = folder
# everything is fine, set the library path
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
@@ -909,6 +876,44 @@ class Library:
session.flush()
logger.info("[Library][Migration][202] Verified TagParent table data")
def __apply_db300_migration(self, session: Session, library_dir: Path):
## remove folder_id column from entries table
# create new table in the desired scheme (without folder_id column)
session.execute(
text("""
CREATE TABLE entries_new (
id INTEGER NOT NULL,
path VARCHAR NOT NULL,
suffix VARCHAR NOT NULL,
date_created DATETIME,
date_modified DATETIME,
date_added DATETIME,
filename TEXT NOT NULL DEFAULT '',
PRIMARY KEY (id),
UNIQUE (path)
)
""")
)
session.flush()
# transfer data to new table
session.execute(
text("""
INSERT INTO entries_new (id, path, suffix, date_created, date_modified, date_added,
filename)
SELECT id, path, suffix, date_created, date_modified, date_added, filename
FROM entries
""")
)
# delete old table
session.execute(text("DROP TABLE entries"))
# rename new table to old table
session.execute(text("ALTER TABLE entries_new RENAME TO entries"))
session.flush()
## drop table "folders"
session.execute(text("DROP TABLE folders"))
session.flush()
@property
def field_templates(self) -> Sequence[BaseFieldTemplate]:
with Session(self.engine) as session:
@@ -927,11 +932,7 @@ class Library:
return entry
def get_entry_full(
self,
entry_id: int,
with_fields: bool = True,
with_tags: bool = True,
with_metadata: bool = True,
self, entry_id: int, with_fields: bool = True, with_tags: bool = True
) -> Entry | None:
"""Load entry and join with all joins and all tags."""
# NOTE: TODO: Currently this method makes multiple separate queries to the db and combines
@@ -961,11 +962,6 @@ class Library:
)
)
if with_metadata:
entry_stmt = entry_stmt.outerjoin(Entry.file_metadata).options(
selectinload(Entry.file_metadata),
)
start_time = time.time()
entry = session.scalar(entry_stmt)
if with_tags:
@@ -1159,79 +1155,10 @@ class Library:
session.query(Entry).where(Entry.id.in_(sub_list)).delete()
session.commit()
def get_entry_id_from_path(self, path: Path) -> int:
"""Attempt to return an Entry ID given a filepath, else return -1."""
def has_entry_with_path(self, path: Path) -> bool:
"""Check if an entry with this path is in the library."""
with Session(self.engine) as session:
return session.scalar(select(Entry.id).where(Entry.path == path).limit(1)) or -1
# def update_entry_file_metadata(
# self, entry_id: int, date_created: datetime | None, date_modified: datetime | None
# ):
# with Session(self.engine) as session:
# stmt = update(FileMetadata).where(
# and_(
# FileMetadata.entry_id == entry_id,
# )
# )
# if date_created:
# stmt = stmt.values(date_created=date_created)
# if date_modified:
# stmt = stmt.values(date_modified=date_modified)
# session.execute(stmt)
# session.commit()
def refresh_file_entry_stats(self, entry_id: int, path: Path | None):
"""Updates a file entry's associated stat() data."""
needs_update = False
entry = self.get_entry_full(
entry_id, with_fields=False, with_tags=False, with_metadata=True
)
if not entry:
return
if not path:
full_path = unwrap(self.library_dir) / entry.path
else:
full_path = unwrap(self.library_dir) / path
logger.info(full_path)
file_date_created = get_date_created(full_path)
file_date_modified = get_date_modified(full_path)
# Log info
if entry.date_created != file_date_created:
logger.info(f"Difference in date_created!: {entry.date_created}/{file_date_created}")
needs_update = True
else:
logger.info("No difference in date_created.")
if entry.date_modified != file_date_modified:
logger.info(f"Difference in date_modified!: {entry.date_modified}/{file_date_modified}")
needs_update = True
else:
logger.info("No difference in date_modified")
if needs_update:
return
else:
logger.info(f"Updating entry file_metadata for {full_path}")
with Session(self.engine) as session:
stmt = update(FileMetadata).where(
and_(
FileMetadata.entry_id == entry_id,
)
)
if file_date_created:
stmt = stmt.values(date_created=file_date_created)
if file_date_modified:
stmt = stmt.values(date_modified=file_date_modified)
session.execute(stmt)
session.commit()
return session.query(exists().where(Entry.path == path)).scalar()
def get_paths(self, limit: int = -1) -> list[str]:
path_strings: list[str] = []
@@ -1549,7 +1476,7 @@ class Library:
Returns True if the action succeeded and False if the path already exists.
"""
if self.get_entry_id_from_path(path) >= 0:
if self.has_entry_with_path(path):
return False
if isinstance(entry_id, Entry):
entry_id = entry_id.id
@@ -1,116 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
from __future__ import annotations
from datetime import datetime as dt
from pathlib import Path
from typing import TYPE_CHECKING, Any, override
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, null
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
from tagstudio.core.library.alchemy.db import Base, PathType
from tagstudio.core.library.alchemy.joins import TagParent
if TYPE_CHECKING:
from tagstudio.core.library.alchemy.models import Entry
class FileMetadata(Base):
"""Table that includes file data and metadata obtained from os.stat() for entries."""
__tablename__ = "file_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
# NOTE: These dates are stored as floats because that's their natural form from os.stat()
# and comparisons are quicker without having to convert to/from datetime objects.
date_created: Mapped[float | None]
date_modified: Mapped[float | None]
def __init__(
self,
entry_id: int,
date_created: float | None = None,
date_modified: float | None = None,
) -> None:
super().__init__()
self.entry_id = entry_id
# # Path data
# self.path = path
# self.filename = path.name
# self.suffix = path.suffix.lstrip(".").lower()
# File metadata
self.date_created = date_created # st_birthtime on Windows and Mac, st_ctime on Linux
self.date_modified = date_modified # st_mtime
class ExifMetadata(Base):
"""Contains Exif metadata for a entries."""
__tablename__ = "exif_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
date_taken: Mapped[dt | None]
def __init__(
self,
entry_id: int,
date_taken: dt | None = None,
) -> None:
super().__init__()
self.entry_id = entry_id
self.date_taken = date_taken # Exif.Image.DateTime
class DimensionMetadata(Base):
"""Contains dimension metadata for entries (e.g. image and video files)."""
__tablename__ = "dimension_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
width: Mapped[int] = mapped_column(nullable=False)
height: Mapped[int] = mapped_column(nullable=False)
def __init__(
self,
entry_id: int,
width: int,
height: int,
) -> None:
super().__init__()
self.entry_id = entry_id
self.width = width
self.height = height
class DurationMetadata(Base):
"""Contains duration metadata for entries (e.g. audio and video files)."""
__tablename__ = "duration_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
duration: Mapped[float] = mapped_column(nullable=False)
def __init__(
self,
entry_id: int,
duration: float,
) -> None:
super().__init__()
self.entry_id = entry_id
self.duration = duration
+13 -48
View File
@@ -17,8 +17,6 @@ from tagstudio.core.library.alchemy.fields import (
TextField,
)
from tagstudio.core.library.alchemy.joins import TagParent
from tagstudio.core.library.alchemy.metadata import FileMetadata
from tagstudio.core.utils.stat import get_date_created, get_date_modified
class Namespace(Base):
@@ -184,31 +182,17 @@ class Tag(Base):
return self.name >= other.name
# TODO: Use or replace these with an actual multi-root implementation
class Folder(Base):
__tablename__ = "folders"
# TODO - implement this
id: Mapped[int] = mapped_column(primary_key=True)
path: Mapped[Path] = mapped_column(PathType, unique=True)
uuid: Mapped[str] = mapped_column(unique=True)
class Entry(Base):
__tablename__ = "entries"
id: Mapped[int] = mapped_column(primary_key=True)
# TODO: Use or replace these with an actual multi-root implementation
folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id"))
folder: Mapped[Folder] = relationship("Folder")
# TODO: Possibly move to FileMetadata table if Entry is split into Entry/FileEntry (see #588)
path: Mapped[Path] = mapped_column(PathType, unique=True)
filename: Mapped[str] = mapped_column()
suffix: Mapped[str] = mapped_column()
date_added: Mapped[dt | None] # The date this entry was added to the library
date_created: Mapped[dt | None]
date_modified: Mapped[dt | None]
date_added: Mapped[dt | None]
tags: Mapped[set[Tag]] = relationship(secondary="tag_entries")
@@ -221,11 +205,6 @@ class Entry(Base):
cascade="all, delete",
)
file_metadata: Mapped["FileMetadata"] = relationship(
uselist=False,
cascade="all, delete-orphan",
)
@property
def fields(self) -> list[BaseField]:
fields: list[BaseField] = []
@@ -241,35 +220,28 @@ class Entry(Base):
def is_archived(self) -> bool:
return any(tag.id == TAG_ARCHIVED for tag in self.tags)
@property
def date_created(self) -> float | None:
return self.file_metadata.date_created if self.file_metadata else None
@property
def date_modified(self) -> float | None:
return self.file_metadata.date_modified if self.file_metadata else None
def __init__(
self,
path: Path,
folder: Folder,
fields: list[BaseField],
id: int | None = None,
date_created: dt | None = None,
date_modified: dt | None = None,
date_added: dt | None = None,
# date_created: float | None = None,
# date_modified: float | None = None,
path_for_file_metadata: Path | None = None,
) -> None:
super().__init__()
self.id = id # pyright: ignore[reportAttributeAccessIssue]
self.folder = folder # NOTE: Currently unused
self.path = path
self.id = id # pyright: ignore[reportAttributeAccessIssue]
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()
self.date_added = date_added # The date this entry was added to the library
# The date the file associated with this entry was created.
# st_birthtime on Windows and Mac, st_ctime on Linux.
self.date_created = date_created
# The date the file associated with this entry was last modified: st_mtime.
self.date_modified = date_modified
# The date this entry was added to the library.
self.date_added = date_added
for field in fields:
if isinstance(field, TextField):
@@ -279,13 +251,6 @@ class Entry(Base):
else:
raise ValueError(f"Invalid field type: {field}")
if path_for_file_metadata:
self.file_metadata = FileMetadata(
entry_id=self.id,
date_created=get_date_created(path_for_file_metadata),
date_modified=get_date_modified(path_for_file_metadata),
)
def has_tag(self, tag: Tag) -> bool:
return tag in self.tags
+2 -14
View File
@@ -8,7 +8,6 @@ from dataclasses import dataclass, field
from datetime import datetime as dt
from pathlib import Path
from time import time
import platform
import structlog
from wcmatch import pathlib
@@ -17,7 +16,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__)
@@ -39,14 +37,11 @@ class RefreshTracker:
while index < len(self.files_not_in_library):
yield index
end = min(len(self.files_not_in_library), index + batch_size)
lib_dir = unwrap(self.library.library_dir)
entries = [
Entry(
path=entry_path,
folder=unwrap(self.library.folder),
fields=[],
date_added=dt.now(),
path_for_file_metadata=(lib_dir / entry_path),
)
for entry_path in self.files_not_in_library[index:end]
]
@@ -147,11 +142,8 @@ class RefreshTracker:
dir_file_count += 1
self.library.included_files.add(f)
entry_id = self.library.get_entry_id_from_path(f)
if entry_id < 0:
if not self.library.has_entry_with_path(f):
self.files_not_in_library.append(f)
else:
self.library.refresh_file_entry_stats(entry_id, path=f)
end_time_total = time()
yield dir_file_count
@@ -195,12 +187,8 @@ class RefreshTracker:
relative_path = f.relative_to(library_dir)
entry_id = self.library.get_entry_id_from_path(relative_path)
if entry_id < 0:
if not self.library.has_entry_with_path(relative_path):
self.files_not_in_library.append(relative_path)
else:
self.library.refresh_file_entry_stats(entry_id, path=relative_path)
except ValueError:
logger.info("[Refresh]: ValueError when refreshing directory with wcmatch!")
+2 -1
View File
@@ -105,6 +105,7 @@ class MediaCategories:
# These sets are used either individually or together to form the final sets
# for the MediaCategory(s).
# These sets may be combined and are NOT 1:1 with the final categories.
_ADOBE_ILLUSTRATOR_SET: set[str] = {".ai"}
_ADOBE_PHOTOSHOP_SET: set[str] = {
".pdd",
".psb",
@@ -580,7 +581,7 @@ class MediaCategories:
)
PDF_TYPES = MediaCategory(
media_type=MediaType.PDF,
extensions=_PDF_SET,
extensions=_PDF_SET | _ADOBE_ILLUSTRATOR_SET,
is_iana=False,
name="pdf",
)
-16
View File
@@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import platform
from pathlib import Path
def get_date_modified(path: Path) -> float:
return path.stat().st_mtime
def get_date_created(path: Path) -> float:
if platform.system() in {"Windows", "Darwin"}:
return path.stat().st_birthtime
else:
return path.stat().st_ctime
+3 -2
View File
@@ -10,7 +10,8 @@ import traceback
import structlog
from tagstudio.core.constants import VERSION, VERSION_BRANCH
from tagstudio.core.constants import BUILD_TYPE, VERSION
from tagstudio.qt.translations import Translations
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
@@ -59,7 +60,7 @@ def main():
"--version",
action="version",
help="Displays TagStudio version information.",
version=f"TagStudio v{VERSION} {VERSION_BRANCH}",
version=f"TagStudio v{VERSION} {Translations[BUILD_TYPE] if BUILD_TYPE else ''}",
)
args = parser.parse_args()
+8 -4
View File
@@ -19,12 +19,12 @@ from PySide6.QtWidgets import (
)
from tagstudio.core.constants import (
BUILD_TYPE,
COPYRIGHT,
DISCORD_URL,
DOCS_URL,
GITHUB_REPO_URL,
VERSION,
VERSION_BRANCH,
)
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
@@ -42,7 +42,12 @@ from tagstudio.qt.views.stylesheets.stylesheets import form_content_style, heade
class AboutModal(QWidget):
"""Modal window showing information about the TagStudio application."""
VERSION_STR: str = f"{Translations['about.version']} {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" # noqa: E501
VERSION_STR: str = " ".join(
[
f"{Translations['about.version']}",
f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}",
]
)
def __init__(self, config_path: Path | str):
super().__init__()
@@ -196,8 +201,7 @@ class AboutModal(QWidget):
# ripgrep Status
ripgrep_path_title = QLabel("ripgrep") # NOTE: Don't localize
ripgrep_path_content = ClickableLabel()
ripgrep_path_content.setText(f"{ripgrep_status}") # TODO: Pass in constructor after #1386
ripgrep_path_content = ClickableLabel(f"{ripgrep_status}")
ripgrep_location = RipgrepStatus.which()
if ripgrep_location:
ripgrep_path_content.clicked.connect(
+5 -4
View File
@@ -1295,11 +1295,12 @@ class ThumbRenderer(QObject):
return im
@staticmethod
def _pdf_thumb(filepath: Path, size: int) -> Image.Image | None:
"""Render a thumbnail for a PDF file.
def _pdf_thumb(filepath: Path, size: int, ext: str) -> Image.Image | None:
"""Render a thumbnail for a PDF or Adobe Illustator file.
filepath (Path): The path of the file.
size (int): The size of the icon.
ext (str): The file extension.
"""
im: Image.Image | None = None
@@ -1321,7 +1322,7 @@ class ThumbRenderer(QObject):
else:
page_size *= size / page_size.width()
# Enlarge image for anti-aliasing
scale_factor = 2.5
scale_factor = 2.5 if ext in {".pdf"} else 1
page_size *= scale_factor
# Render image with no anti-aliasing for speed
render_options: QPdfDocumentRenderOptions = QPdfDocumentRenderOptions()
@@ -1910,7 +1911,7 @@ class ThumbRenderer(QObject):
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.PDF_TYPES, mime_fallback=True
):
image = self._pdf_thumb(_filepath, adj_size)
image = self._pdf_thumb(_filepath, adj_size, ext)
# Archives =====================================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.ARCHIVE_TYPES):
image = self._archive_thumb(_filepath, ext)
+2 -2
View File
@@ -40,7 +40,7 @@ from PySide6.QtGui import (
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton, QScrollArea
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
from tagstudio.core.constants import BUILD_TYPE, TAG_ARCHIVED, TAG_FAVORITE, VERSION
from tagstudio.core.driver import DriverMixin
from tagstudio.core.enums import AppCacheItems, MacroID, ShowFilepathOption
from tagstudio.core.library.alchemy.enums import BrowsingState, SortingModeEnum
@@ -203,7 +203,7 @@ class QtDriver(DriverMixin, QObject):
self.scrollbar_pos = 0
self.spacing = None
self.branch: str = (" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else ""
self.branch: str = (" (" + Translations[BUILD_TYPE] + ")") if BUILD_TYPE else ""
self.base_title: str = f"TagStudio Alpha {VERSION}{self.branch}"
# self.title_text: str = self.base_title
# self.buffer = {}
+7 -2
View File
@@ -10,7 +10,7 @@ from PySide6.QtCore import QRect, Qt
from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap
from PySide6.QtWidgets import QSplashScreen, QWidget
from tagstudio.core.constants import COPYRIGHT, COPYRIGHT_COMPACT, VERSION, VERSION_BRANCH
from tagstudio.core.constants import BUILD_TYPE, COPYRIGHT, COPYRIGHT_COMPACT, VERSION
from tagstudio.qt.global_settings import Splash
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
@@ -21,7 +21,12 @@ logger = structlog.get_logger(__name__)
class SplashScreen:
"""The custom splash screen widget for TagStudio."""
VERSION_STR: str = f"{Translations['about.version']} {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" # noqa: E501
VERSION_STR: str = " ".join(
[
f"{Translations['about.version']}",
f"{VERSION} {(' (' + Translations[BUILD_TYPE] + ')') if BUILD_TYPE else ''}",
]
)
DEFAULT_SPLASH = Splash.AURORA
def __init__(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

@@ -10,6 +10,7 @@
"about.version.latest": "{built_version} (Latest Release: {latest_version})",
"about.website": "Website",
"app.git": "Git Commit",
"app.nightly": "Nightly",
"app.pre_release": "Pre-Release",
"app.title": "{base_title} - Library '{library_dir}'",
"color_manager.title": "Manage Tag Colors",
+10 -10
View File
@@ -1,5 +1,5 @@
{
"about.app_cache_path": "Ruta caché aplicación",
"about.app_cache_path": "Ruta de la Caché de la Aplicación",
"about.config_path": "Ruta de Configuración",
"about.description": "TagStudio es una aplicación para organizar fotografías y archivos que utiliza un sistema de etiquetas subyacentes centrado en dar libertad y flexibilidad al usuario. Sin programas ni formatos propios, ni un mar de archivos y sin trastornar completamente la estructura de tu sistema de archivos.",
"about.documentation": "Documentación",
@@ -8,14 +8,14 @@
"about.title": "Acerca de TagStudio",
"about.version": "Versión",
"about.version.latest": "{built_version} (Última versión: {latest_version})",
"about.website": "Página web",
"about.website": "Página Web",
"app.git": "Commit de Git",
"app.pre_release": "Pre-Lanzamiento",
"app.title": "{base_title} - Biblioteca '{library_dir}'",
"color.color_border": "Usar color secundario para el Borde",
"color.color_border": "Usar Color Secundario para Borde",
"color.confirm_delete": "¿Estás seguro de que quieres eliminar el color \"{color_name}\"?",
"color.delete": "Eliminar Etiqueta",
"color.import_pack": "Importar paquete de colores",
"color.import_pack": "Importar Paquete de Colores",
"color.name": "Nombre",
"color.namespace.delete.prompt": "¿Estás seguro de que quieres eliminar el espacio de nombres de este color? ¡Esto eliminará todos los colores en el espacio de nombres junto con él!",
"color.namespace.delete.title": "Eliminar el espacio de nombres de color",
@@ -25,7 +25,7 @@
"color.primary_required": "Color primario (Obligatorio)",
"color.secondary": "Color secundario",
"color.title.no_color": "Sin color",
"color_manager.title": "Administrar los colores de las etiquetas",
"color_manager.title": "Administrar Colores de Etiquetas",
"dependency.missing.title": "{dependency} no encontrada",
"drop_import.description": "Los siguientes archivos igualan con las rutas de archivos que ya existen en la biblioteca",
"drop_import.duplicates_choice.plural": "Los siguientes {count} archivos igualan con las rutas de archivos que ya existen en la biblioteca.",
@@ -227,12 +227,12 @@
"language.tr": "Turco",
"language.zh_Hans": "Chino (simplificado)",
"language.zh_Hant": "Chino (tradicional)",
"library.missing": "Falta la ubicación",
"library.missing": "Falta la Ubicación de la Biblioteca",
"library.name": "Biblioteca",
"library.refresh.scanning.plural": "Escaneando directorios en busca de nuevos archivos...\n{searched_count} archivos buscados, {found_count} nuevos archivos encontrados",
"library.refresh.scanning.singular": "Escaneando directorios en busca de nuevos archivos...\n{searched_count} Archivos buscados, {found_count} Nuevos archivos encontrados",
"library.refresh.scanning_preparing": "Buscar archivos nuevos en los directorios...\nPreparando...",
"library.refresh.title": "Refrescar directorios",
"library.refresh.scanning_preparing": "Buscando archivos nuevos en los directorios...\nPreparando...",
"library.refresh.title": "Refrescando directorios",
"library.scan_library.title": "Escaneando la biblioteca",
"library_info.cleanup": "Limpieza",
"library_info.cleanup.backups": "Reespaldos de la Librería:",
@@ -387,7 +387,7 @@
"tag.is_category": "Es categoría",
"tag.is_hidden": "Está oculto",
"tag.name": "Nombre",
"tag.new": "Nueva etiqueta",
"tag.new": "Nueva Etiqueta",
"tag.parent_tags": "Etiquetas principales",
"tag.parent_tags.add": "Añadir etiquetas principales",
"tag.parent_tags.description": "Esta etiqueta se puede tratar como sustituto de cualquiera de las etiquetas padre en las búsquedas.",
@@ -417,7 +417,7 @@
"view.size.1": "Pequeño",
"view.size.2": "Medio",
"view.size.3": "Grande",
"view.size.4": "Extra grande",
"view.size.4": "Extra Grande",
"window.message.error_opening_library": "Error abriendo la biblioteca.",
"window.title.error": "Error",
"window.title.open_create_library": "Abrir/Crear biblioteca"
@@ -4,6 +4,7 @@
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui met en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
"about.documentation": "Documentation",
"about.module.found": "Trouvé",
"about.modules.title": "Modules optionnels",
"about.title": "À propos de TagStudio",
"about.version": "Version",
"about.version.latest": "{built_version} (Dernière version : {latest_version})",
@@ -196,6 +197,36 @@
"json_migration.title.new_lib": "<h2>Bibliothèque v9.5+</h2>",
"json_migration.title.old_lib": "<h2>Bibliothèque v9.4</h2>",
"landing.open_create_library": "Ouvrir/Créer une Bibliothèque {shortcut}",
"language.am": "Amharic",
"language.ceb": "Cébuano",
"language.cs": "Tchèque",
"language.da": "Danois",
"language.de": "Allemand",
"language.el": "Grec",
"language.en": "Anglais",
"language.es": "Espagnol",
"language.fi": "Finnois",
"language.fil": "Philippin",
"language.fr": "Français",
"language.hu": "Hongrois",
"language.is": "Islandais",
"language.it": "Italien",
"language.ja": "Japonais",
"language.nb_NO": "Norvégien (Bokmål)",
"language.nl": "Néerlandais",
"language.pl": "Polonais",
"language.pt": "Portugais",
"language.pt_BR": "Portugais (Brésil)",
"language.qpv": "Viossa",
"language.ro": "Roumain",
"language.ru": "Russe",
"language.sv": "Suédois",
"language.ta": "Tamil",
"language.th": "Thaï",
"language.tok": "Toki Pona",
"language.tr": "Turc",
"language.zh_Hans": "Chinois (Simplifier)",
"language.zh_Hant": "Chinois (Traditionnelle)",
"library.missing": "Emplacement Manquant",
"library.name": "Bibliothèque",
"library.refresh.scanning.plural": "Analyse du Répertoire pour de Nouveaux Fichiers...\n{searched_count} Fichiers Trouvées, {found_count} Nouveaux Fichiers",
@@ -277,6 +308,8 @@
"select.all": "Tout Sélectionner",
"select.clear": "Effacer la Sélection",
"select.inverse": "Inverser la Sélection",
"settings.appearance": "Apparence",
"settings.cached_thumb_resolution.label": "Résolution des vignettes mises en cache",
"settings.clear_thumb_cache.title": "Effacer le cache des vignettes",
"settings.dateformat.english": "Anglais",
"settings.dateformat.international": "International",
@@ -292,6 +325,8 @@
"settings.infinite_scroll": "Défilement continu",
"settings.language": "Langage",
"settings.library": "Paramètres de la Bibliothèque",
"settings.localization": "Localisation",
"settings.media": "Médias",
"settings.open_library_on_start": "Ouvrir la Bibliothèque au Démarrage",
"settings.page_size": "Entités par page",
"settings.restart_required": "Veuillez redémarré TagStudio pour que les changements prenne effet.",
@@ -10,6 +10,7 @@
"about.version.latest": "{built_version} (legújabb kiadás: {latest_version})",
"about.website": "Honlap",
"app.git": "Git-véglegesítés",
"app.nightly": "Napi",
"app.pre_release": "Kísérleti verzió",
"app.title": "{base_title} Könyvtár: „{library_dir}”",
"color.color_border": "Másodlagos szín használata keretszínként",
@@ -1,9 +1,11 @@
{
"about.app_cache_path": "TagStudio缓存路径",
"about.config_path": "配置路径",
"about.description": "TagStudio是一款照片和文件组织应用程序,采用基于标签的系统,旨在为用户提供自由和灵活性。该应用程序不使用专有程序或格式,不会产生大量的辅助文件,也不会对您的文件系统结构造成彻底的颠覆。",
"about.documentation": "文档",
"about.module.found": "存在",
"about.title": "关于 TagStudio",
"about.version": "版本",
"about.website": "网站",
"app.git": "Git 提交更新",
"app.pre_release": "预发布版本",
-8
View File
@@ -20,7 +20,6 @@ sys.path.insert(0, str(CWD.parent))
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.thumb_grid_layout import ThumbGridLayout
from tagstudio.qt.ts_qt import QtDriver
@@ -36,22 +35,18 @@ def file_mediatypes_library():
status = lib.open_library(Path(""), in_memory=True)
assert status.success
folder = unwrap(lib.folder)
entry1 = Entry(
folder=folder,
path=Path("foo.png"),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
entry2 = Entry(
folder=folder,
path=Path("bar.png"),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
entry3 = Entry(
folder=folder,
path=Path("baz.apng"),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
@@ -87,7 +82,6 @@ def library(request, library_dir: Path): # pyright: ignore
lib = Library()
status = lib.open_library(library_path, in_memory=True)
assert status.success
folder = unwrap(lib.folder)
tag = Tag(
name="foo",
@@ -116,7 +110,6 @@ def library(request, library_dir: Path): # pyright: ignore
# default item with deterministic name
entry = Entry(
id=1,
folder=folder,
path=Path("foo.txt"),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
@@ -124,7 +117,6 @@ def library(request, library_dir: Path): # pyright: ignore
entry2 = Entry(
id=2,
folder=folder,
path=Path("one/two/bar.md"),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
Binary file not shown.
-4
View File
@@ -8,25 +8,21 @@ from tagstudio.core.library.alchemy.fields import BaseField, TextField
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.library.alchemy.registries.dupe_files_registry import DupeFilesRegistry
from tagstudio.core.utils.types import unwrap
CWD = Path(__file__).parent
def test_refresh_dupe_files(library: Library):
library.library_dir = Path("/tmp/")
folder = unwrap(library.folder)
fields: list[BaseField] = [TextField(name="Title", value="I'm a Test Title")]
entry = Entry(
folder=folder,
path=Path("bar/foo.txt"),
fields=fields,
)
entry2 = Entry(
folder=folder,
path=Path("foo/foo.txt"),
fields=fields,
)
+5 -14
View File
@@ -77,13 +77,12 @@ def test_library_add_file(library: Library):
"""Check Entry.path handling for insert vs lookup"""
entry = Entry(
path=Path("bar.txt"),
folder=unwrap(library.folder),
fields=[TextField(name="Title", value="I'm a Test Title")],
)
assert not library.get_entry_id_from_path(entry.path)
assert not library.has_entry_with_path(entry.path)
assert library.add_entries([entry])
assert library.get_entry_id_from_path(entry.path)
assert library.has_entry_with_path(entry.path)
def test_create_tag(library: Library, generate_tag: Callable[..., Tag]):
@@ -139,8 +138,7 @@ def test_get_entry(library: Library, entry_min: Entry):
def test_entries_count(library: Library):
folder = unwrap(library.folder)
entries = [Entry(path=Path(f"{x}.txt"), folder=folder, fields=[]) for x in range(10)]
entries = [Entry(path=Path(f"{x}.txt"), fields=[]) for x in range(10)]
new_ids = library.add_entries(entries)
assert len(new_ids) == 10
@@ -254,7 +252,6 @@ def test_update_entry_with_multiple_identical_text_fields(library: Library, entr
def test_mirror_entry_fields(library: Library):
# Create and add entries with fields
entry_a = Entry(
folder=unwrap(library.folder),
path=Path("title_and_date.txt"),
fields=[
TextField(name="Title", value="I'm a Test Title"),
@@ -262,7 +259,6 @@ def test_mirror_entry_fields(library: Library):
],
)
entry_b = Entry(
folder=unwrap(library.folder),
path=Path("notes.txt"),
fields=[
TextField(name="Notes", value="These are my notes.\nNo peeking!", is_multiline=True),
@@ -270,7 +266,6 @@ def test_mirror_entry_fields(library: Library):
],
)
entry_c = Entry(
folder=unwrap(library.folder),
path=Path("date_published.txt"),
fields=[
DatetimeField(name="Date Published", value="2000-01-01 12:00:00"),
@@ -319,14 +314,11 @@ def test_mirror_entry_fields(library: Library):
def test_merge_entries(library: Library):
folder = unwrap(library.folder)
tag_0: Tag = unwrap(library.add_tag(Tag(id=1010, name="tag_0")))
tag_1: Tag = unwrap(library.add_tag(Tag(id=1011, name="tag_1")))
tag_2: Tag = unwrap(library.add_tag(Tag(id=1012, name="tag_2")))
entry_a = Entry(
folder=folder,
path=Path("a"),
fields=[
TextField(name="Author", value="Author McAuthorson"),
@@ -334,7 +326,6 @@ def test_merge_entries(library: Library):
],
)
entry_b = Entry(
folder=folder,
path=Path("b"),
fields=[TextField(name="Notes", value="test note", is_multiline=True)],
)
@@ -347,8 +338,8 @@ def test_merge_entries(library: Library):
entry_b_: Entry = unwrap(library.get_entry_full(entry_b_id))
assert library.merge_entries(entry_a_, entry_b_)
assert not library.get_entry_id_from_path(Path("a"))
assert library.get_entry_id_from_path(Path("b"))
assert not library.has_entry_with_path(Path("a"))
assert library.has_entry_with_path(Path("b"))
entry_b_merged = unwrap(library.get_entry_full(entry_b_id))