Compare commits

..

13 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
Travis Abendshien 6aa0cf74f9 chore: removed unused collage_icon.py file 2026-07-12 21:08:31 -07:00
Travis Abendshien 49b450c3a4 fix: don't prevent greater minor version DBs from opening 2026-07-12 20:58:53 -07:00
Ludvig Sandh a1dfa62e4a fix: correctly display duration statistic in preview panel (#1421)
* fix: correctly display duration statistic in preview panel

* style: format file_attributes.py with ruff

* refactor: consolidate preview thumb current file state

* refactor: remove another redundant current file state

* refactor: move generic format_duration function to string formatting util file

* refactor: tighten signal's declared types
2026-07-12 13:16:14 -07:00
26 changed files with 189 additions and 202 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"
@@ -582,7 +582,7 @@ class Library:
session.commit()
logger.info(f"[Library][Migration][{v}] Completed DB Migration")
assert loaded_db_version == DB_VERSION, (
assert loaded_db_version >= DB_VERSION, (
"Ran all migrations, but the DB is still not on the newest version"
)
logger.info(f"[Library] Library migrated to DB version {DB_VERSION}")
+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",
)
@@ -49,3 +49,14 @@ def is_version_outdated(current: str, latest: str) -> bool:
return vcur.patch < vlat.patch
else:
return vcur.prerelease is not None or vcur.build is not None
def format_duration(duration: int | float) -> str:
"""Format a duration in seconds as M:SS or H:MM:SS."""
try:
seconds = int(float(duration))
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
return f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes}:{seconds:02}"
except (OverflowError, ValueError):
return "-:--"
+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()
@@ -32,8 +32,6 @@ Image.MAX_IMAGE_PIXELS = None
class PreviewThumb(PreviewThumbView):
__current_file: Path
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__(library, driver)
@@ -114,7 +112,7 @@ class PreviewThumb(PreviewThumbView):
def display_file(self, filepath: Path) -> FileAttributeData:
"""Render a single file preview."""
self.__current_file = filepath
self._current_file = filepath
ext = filepath.suffix.lower()
@@ -150,21 +148,26 @@ class PreviewThumb(PreviewThumbView):
@override
def _open_file_action_callback(self):
open_file(
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
)
if self._current_file:
open_file(
self._current_file,
windows_start_command=self.__driver.settings.windows_start_command,
)
@override
def _open_explorer_action_callback(self):
open_file(self.__current_file, file_manager=True)
if self._current_file:
open_file(self._current_file, file_manager=True)
@override
def _delete_action_callback(self):
if bool(self.__current_file):
self.__driver.delete_files_callback(self.__current_file)
if self._current_file:
self.__driver.delete_files_callback(self._current_file)
@override
def _button_wrapper_callback(self):
open_file(
self.__current_file, windows_start_command=self.__driver.settings.windows_start_command
)
if self._current_file:
open_file(
self._current_file,
windows_start_command=self.__driver.settings.windows_start_command,
)
+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(
-133
View File
@@ -1,133 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
import math
from pathlib import Path
import cv2
import structlog
from PIL import Image, ImageChops, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PySide6.QtCore import QObject, Signal
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.helpers.file_tester import is_readable_video
logger = structlog.get_logger(__name__)
class CollageIconRenderer(QObject):
rendered = Signal(Image.Image)
done = Signal()
def __init__(self, library: Library):
QObject.__init__(self)
self.lib = library
def render(
self,
entry_id: int,
size: tuple[int, int],
data_tint_mode: bool,
data_only_mode: bool,
keep_aspect: bool,
):
entry = unwrap(self.lib.get_entry(entry_id))
filepath = unwrap(self.lib.library_dir) / entry.path
color: str = ""
try:
if data_tint_mode or data_only_mode:
color = "#28bb48" if entry.tags else "#e22c3c"
if data_only_mode:
pic = Image.new("RGB", size, color)
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
if not data_only_mode:
logger.info(
"Combining icons",
entry=entry,
color=self.get_file_color(filepath.suffix.lower()),
)
ext: str = filepath.suffix.lower()
if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES):
try:
with Image.open(filepath) as pic:
if keep_aspect:
pic.thumbnail(size)
else:
pic = pic.resize(size)
if data_tint_mode and color:
pic = pic.convert(mode="RGB")
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
self.rendered.emit(pic)
except DecompressionBombError as e:
logger.info(f"[ERROR] One of the images was too big ({e})")
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.VIDEO_TYPES
) and is_readable_video(filepath):
video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG)
video.set(
cv2.CAP_PROP_POS_FRAMES,
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
)
success, frame = video.read()
# NOTE: Depending on the video format, compression, and
# frame count, seeking halfway does not work and the thumb
# must be pulled from the earliest available frame.
max_frame_seek: int = 10
for i in range(
0,
min(
max_frame_seek,
math.floor(video.get(cv2.CAP_PROP_FRAME_COUNT)),
),
):
success, frame = video.read()
if not success:
video.set(cv2.CAP_PROP_POS_FRAMES, i)
else:
break
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
with Image.fromarray(frame, mode="RGB") as pic:
if keep_aspect:
pic.thumbnail(size)
else:
pic = pic.resize(size)
if data_tint_mode and color:
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
self.rendered.emit(pic)
except (UnidentifiedImageError, FileNotFoundError):
logger.error("Couldn't read entry", entry=entry.path)
with Image.open(
str(Path(__file__).parents[1] / "resources/qt/images/thumb_broken_512.png")
) as pic:
pic.thumbnail(size)
if data_tint_mode and color:
pic = pic.convert(mode="RGB")
pic = ImageChops.hard_light(pic, Image.new("RGB", size, color))
# collage.paste(pic, (y*thumb_size, x*thumb_size))
self.rendered.emit(pic)
except KeyboardInterrupt:
logger.info("Collage operation cancelled.")
except Exception:
logger.exception("render failed", entry=entry.path)
self.done.emit()
def get_file_color(self, ext: str):
if ext.lower() == "gif":
return "\033[93m"
if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES):
return "\033[37m"
elif MediaCategories.is_ext_in_category(ext, MediaCategories.VIDEO_TYPES):
return "\033[96m"
elif MediaCategories.is_ext_in_category(ext, MediaCategories.PLAINTEXT_TYPES):
return "\033[92m"
else:
return "\033[97m"
+2 -10
View File
@@ -7,7 +7,6 @@ import platform
import typing
from dataclasses import dataclass
from datetime import datetime as dt
from datetime import timedelta
from pathlib import Path
import structlog
@@ -20,6 +19,7 @@ from tagstudio.core.enums import ShowFilepathOption
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.ignore import Ignore
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.utils.str_formatting import format_duration
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
@@ -224,15 +224,7 @@ class FileAttributes(QWidget):
if stats.duration is not None:
stats_label_text = add_newline(stats_label_text)
try:
dur_str = str(timedelta(seconds=float(stats.duration)))[:-7]
if dur_str.startswith("0:"):
dur_str = dur_str[2:]
if dur_str.startswith("0"):
dur_str = dur_str[1:]
except OverflowError:
dur_str = "-:--"
stats_label_text += f"{dur_str}"
stats_label_text += format_duration(stats.duration)
if font_family:
stats_label_text = add_newline(stats_label_text)
+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 = {}
@@ -51,6 +51,7 @@ class PreviewPanelView(QWidget):
self._containers = FieldContainers(
self.lib, driver
) # TODO: this should be name mangled, but is still needed on the controller side atm
self.__current_stats: FileAttributeData | None = None
preview_section = QWidget()
preview_layout = QVBoxLayout(preview_section)
@@ -132,6 +133,7 @@ class PreviewPanelView(QWidget):
def __connect_callbacks(self) -> None:
self.__add_field_button.clicked.connect(self._add_field_button_callback)
self.__add_tag_button.clicked.connect(self._add_tag_button_callback)
self._thumb.stats_updated.connect(self.__thumb_stats_updated_callback)
def _add_field_button_callback(self) -> None:
raise NotImplementedError()
@@ -139,6 +141,25 @@ class PreviewPanelView(QWidget):
def _add_tag_button_callback(self) -> None:
raise NotImplementedError()
def __thumb_stats_updated_callback(self, filepath: Path, stats: FileAttributeData) -> None:
if len(self._selected) != 1:
return
if filepath != self._thumb.current_file:
return
if self.__current_stats is None:
self.__current_stats = FileAttributeData()
if stats.width is not None:
self.__current_stats.width = stats.width
if stats.height is not None:
self.__current_stats.height = stats.height
if stats.duration is not None:
self.__current_stats.duration = stats.duration
self._file_attrs.update_stats(filepath, self.__current_stats)
def _set_selection_callback(self) -> None:
raise NotImplementedError()
@@ -155,6 +176,7 @@ class PreviewPanelView(QWidget):
# No Items Selected
if len(selected) == 0:
self._thumb.hide_preview()
self.__current_stats = None
self._file_attrs.update_stats()
self._file_attrs.update_date_label()
self._containers.hide_containers()
@@ -167,9 +189,12 @@ class PreviewPanelView(QWidget):
entry: Entry = unwrap(self.lib.get_entry(entry_id))
filepath: Path = unwrap(self.lib.library_dir) / entry.path
if filepath != self._thumb.current_file:
self.__current_stats = None
if update_preview:
stats: FileAttributeData = self._thumb.display_file(filepath)
self.__current_stats = stats
self._file_attrs.update_stats(filepath, stats)
self._file_attrs.update_date_label(filepath)
self._containers.update_from_entry(entry_id)
@@ -182,6 +207,7 @@ class PreviewPanelView(QWidget):
elif len(selected) > 1:
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
self._thumb.hide_preview() # TODO: Render mixed selection
self.__current_stats = None
self._file_attrs.update_multi_selection(len(selected))
self._file_attrs.update_date_label()
self._containers.hide_containers() # TODO: Allow for mixed editing
+43 -13
View File
@@ -34,11 +34,13 @@ class PreviewThumbView(QWidget):
"""The Preview Panel Widget."""
check_ffmpeg = Signal(bool)
stats_updated = Signal(Path, FileAttributeData)
__img_button_size: tuple[int, int]
__image_ratio: float
__filepath: Path | None
_current_file: Path | None
__should_render_on_resize: bool
__rendered_res: tuple[int, int]
def __init__(self, library: Library, driver: "QtDriver") -> None:
@@ -47,6 +49,8 @@ class PreviewThumbView(QWidget):
self.__img_button_size = (266, 266)
self.__image_ratio = 1.0
self.__should_render_on_resize = False
self.__image_layout = QStackedLayout(self)
self.__image_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.__image_layout.setStackingMode(QStackedLayout.StackingMode.StackAll)
@@ -92,6 +96,10 @@ class PreviewThumbView(QWidget):
self.__media_player.addAction(open_file_action)
self.__media_player.addAction(open_explorer_action)
self.__media_player.addAction(delete_action)
# QMediaPlayer loads duration asynchronously after setSource().
self.__media_player.player.durationChanged.connect(
self.__media_player_duration_changed_callback
)
# Need to watch for this to resize the player appropriately.
self.__media_player.player.hasVideoChanged.connect(
@@ -128,6 +136,16 @@ class PreviewThumbView(QWidget):
def __media_player_video_changed_callback(self, video: bool) -> None:
self.__update_image_size((self.size().width(), self.size().height()))
def __media_player_duration_changed_callback(self, duration_ms: int) -> None:
filepath = self.__media_player.filepath
if filepath is None or duration_ms <= 0:
return
self.stats_updated.emit(
filepath,
FileAttributeData(duration=duration_ms // 1000),
)
def __thumb_renderer_updated_callback(
self, _timestamp: float, img: QPixmap, _size: QSize, _path: Path
) -> None:
@@ -207,7 +225,8 @@ class PreviewThumbView(QWidget):
self.__preview_gif.hide()
def __render_thumb(self, filepath: Path) -> None:
self.__filepath = filepath
self.__should_render_on_resize = True
self.__rendered_res = (
math.ceil(self.__img_button_size[0] * THUMB_SIZE_FACTOR),
math.ceil(self.__img_button_size[1] * THUMB_SIZE_FACTOR),
@@ -221,17 +240,16 @@ class PreviewThumbView(QWidget):
update_on_ratio_change=True,
)
def __update_media_player(self, filepath: Path) -> int:
"""Display either audio or video.
Returns the duration of the audio / video.
"""
def __update_media_player(self, filepath: Path) -> None:
"""Display either audio or video."""
self.__media_player.play(filepath)
return self.__media_player.player.duration() * 1000
def _display_video(self, filepath: Path, size: QSize | None) -> FileAttributeData:
self.__should_render_on_resize = False
self.__switch_preview(MediaType.VIDEO)
stats = FileAttributeData(duration=self.__update_media_player(filepath))
self.__update_media_player(filepath)
stats = FileAttributeData()
if size is not None:
stats.width = size.width()
@@ -250,10 +268,13 @@ class PreviewThumbView(QWidget):
def _display_audio(self, filepath: Path) -> FileAttributeData:
self.__switch_preview(MediaType.AUDIO)
self.__render_thumb(filepath)
return FileAttributeData(duration=self.__update_media_player(filepath))
self.__update_media_player(filepath)
return FileAttributeData()
def _display_gif(self, gif_data: bytes, size: tuple[int, int]) -> FileAttributeData | None:
"""Update the animated image preview from a filepath."""
self.__should_render_on_resize = False
stats = FileAttributeData()
# Ensure that any movie and buffer from previous animations are cleared.
@@ -296,17 +317,26 @@ class PreviewThumbView(QWidget):
def hide_preview(self) -> None:
"""Completely hide the file preview."""
self.__switch_preview(None)
self.__filepath = None
self._current_file = None
self.__should_render_on_resize = False
@override
def resizeEvent(self, event: QResizeEvent) -> None:
self.__update_image_size((self.size().width(), self.size().height()))
if self.__filepath is not None and self.__rendered_res < self.__img_button_size:
self.__render_thumb(self.__filepath)
if (
self._current_file is not None
and self.__should_render_on_resize
and self.__rendered_res < self.__img_button_size
):
self.__render_thumb(self._current_file)
return super().resizeEvent(event)
@property
def media_player(self) -> MediaPlayer:
return self.__media_player
@property
def current_file(self) -> Path | None:
return self._current_file
+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": "预发布版本",