mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-23 05:53:59 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5efd153908 | |||
| e106e9759c | |||
| 5c282e0ada | |||
| de1d8e5c1b | |||
| c3b6bec2ff | |||
| 58496a7d2d | |||
| dc4251ff55 | |||
| 174262b9b3 | |||
| 27d761731c | |||
| 51a9c16f50 | |||
| 6aa0cf74f9 | |||
| 49b450c3a4 | |||
| a1dfa62e4a |
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 "-:--"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "预发布版本",
|
||||
|
||||
Reference in New Issue
Block a user