Compare commits

...

10 Commits

Author SHA1 Message Date
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
Xarvex aa2d9d4815 fix(ci): misapplied rebase, cont 9d5200b
fixes: #1445 (for good)
2026-07-11 19:33:29 -05:00
Xarvex 16cfa8d2ff chore(ci): ignore translations pushes for reuse 2026-07-11 19:27:21 -05:00
Xarvex 9d5200b2f2 fix(ci): path matching fixes, corrected concurrency
Pull requests are now correctly handled and concurrency is per-job.
translations branch can safely be ignored for pushes.
2026-07-11 19:19:42 -05:00
Jann Stute f252a86fd5 refactor: split out sql migrations from library (#1432)
* refactor: minor simplification

* refactor: split open_library into new and not new case

Functionality is entirely unchanged, but entire method is duplicated.
Removing dead code from either copy is next.

* refactor: remove dead code

* doc: add todos and remove trivially true assert

* refactor: add assurance 1 version check

Assurance 1 was introduced with library version 101, specifically commmit 12e074b71d.
Thus all libraries created on version 101 and above already fullfill it and don't need it.
Furthermore, it only fails if the library didn't need the assurance, meaning the try-except and warning catching can be removed

* refactor: remove various unnecessary try-except statements in new_lib

All of these were introduced to account for the differences between new libraries and existing ones,
which makes them unnecessary after this split.

* refactor: remove unnecessary check in new_lib

* refactor: move folder assurance after migrations

The folder assurance has been present since the very first SQL commit e5e7b8afc6,
and thus only has an effect when the library is moved, meaning that is semantically not part of the migrations.

* refactor: massively simplify open_library

* refactor: move engine creation to static method

* refactor: add version check for assurance 3

Assurance 3 was introduced in commit 47c3d5338f
shortly (24h 20min) after the DB version had been bumped to 200.
Since it was also included in the first release with DB version 200,
all libraries created on DB version 200 and above
can be presumed to have already had this applied on creation.

* refactor: add assurance 3 to DB 200 migration

* refactor: move assurance 1 to a proper migration method

Moving the assurance after the migrations 7, 8, 9, and 100 is fine because it doesn't affect those.

* refactor: update version after every successfull migration

Since every migration migrates the library to a certain DB_VERSION, we can simply update the library version after such a successfull migration.
This way if a migration fails, the previous migrations won't be rerun the next time the library is opened.

* refactor: rewrite migration procedure as loop

* refactor: apply migration and update version in same transaction

None of the content of the migrations is changed, only the `with session:` statements are removed.

* refactor: replace all commits in the migrations with flushes

Also removes various try-except statements in the migrations.
These were introduced to catch the case where the migration is run twice due to a later migration failing,
this is not necessary anymore as every migration will only be executed at most once per library.

* refactor: make sure the migration log statements are consistent

* fix: pass library dir to migrations

* fix(db migration 8): only add colors that are actually new

DB Migration 8 was previously adding all colors except for the neon ones,
however, all but three of those have been around since DB version 4,
meaning that they don't need to be added at all (which caused the tests to fail).

* fix: json migration used outdated interface

* fix(open_library): create TS directory only if not opened in memory

* fix: enable sane transaction behaviour

By default in SQLAlchemy schema changes are automatically committed,
even if auto-commit is turned off.
This commit turns off auto-commit completely,
which caused some schema to not be applied correctly,
but those were fixed here as well.

* refactor: hide 'argument is not accessed' notices

* fix(db migration 9): filename property wasn't written correctly

* fix(db migration 104): include/exclude list was loaded incorrectly

* feat: log start and end of DB migrations

* fix: don't use double transaction in open_sqlite_library

* fix: only commit once when creating new library

* doc: add comment on removing Folder logic

* refactor: remove dunder naming
2026-07-11 12:36:55 -07:00
Xarvex a0fb679729 fix(ci): more path matching 2026-07-11 01:29:11 -05:00
Xarvex b182b2ff7e chore(ci): more paths for pytest to trigger on 2026-07-09 22:23:17 -05:00
purpletennisball 308b36b31e fix: thumbnail rendering for older pxd files (#1441) 2026-07-09 18:03:47 -07:00
12 changed files with 617 additions and 648 deletions
+64 -12
View File
@@ -8,26 +8,29 @@ on:
paths: &on_paths
- .github/actions/setup-python/action.yml
- .github/workflows/checks_python.yml
- src/**/resources/**
- src/**/*.json
- src/**/*.qrc
- tests/**
- .editorconfig
- pyproject.toml
- uv.lock
- '**.py'
- '**.pyi'
- 'src/tagstudio/resources/**'
- '**.pyi?'
push:
branches-ignore:
- translations
paths: *on_paths
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: sh
jobs:
run-conditions:
permissions:
pull-requests: read
name: Run Conditions
runs-on: ubuntu-latest
outputs:
@@ -36,14 +39,25 @@ jobs:
ruff: ${{ steps.run-conditions.outputs.ruff }}
steps:
- name: Checkout repository
if: github.event_name != 'pull_request'
uses: actions/checkout@v7
with:
fetch-depth: 0
# Largest positive number; infinite depth.
# Using 0 would grab all branches.
# See: https://github.com/actions/checkout/issues/520
# See: https://stackoverflow.com/questions/6802145/how-to-convert-a-git-shallow-clone-to-a-full-clone/6802238#6802238
# `git fetch --unshallow` as suggested in later answers would be an extra operation.
fetch-depth: '2147483647'
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v47.0.6
with:
fail_on_initial_diff_error: 'true'
fail_on_submodule_diff_error: 'true'
skip_initial_fetch: 'true'
# WARNING: Does not support `?` glob operand!
files_yaml: |
generic:
- .github/workflows/checks_python.yml
@@ -55,20 +69,51 @@ jobs:
- .github/actions/setup-python/action.yml
pytest:
- .github/actions/setup-python/action.yml
- 'src/tagstudio/resources/**'
- src/**/resources/**
- src/**/*.json
- src/**/*.qrc
- tests/**
ruff:
- .editorconfig
- name: Set run conditions
id: run-conditions
env:
CHANGED_GENERIC: ${{ steps.changed-files.outputs.generic_any_changed }}
CHANGED_PYRIGHT: ${{ steps.changed-files.outputs.pyright_any_changed }}
CHANGED_PYTEST: ${{ steps.changed-files.outputs.pytest_any_changed }}
CHANGED_RUFF: ${{ steps.changed-files.outputs.ruff_any_changed }}
run: |
pyright=false
pytest=false
ruff=false
if [ "${CHANGED_GENERIC}" = true ]; then
pyright=true
pytest=true
ruff=true
else
if [ "${CHANGED_PYRIGHT}" = true ]; then
pyright=true
fi
if [ "${CHANGED_PYTEST}" = true ]; then
pytest=true
fi
if [ "${CHANGED_RUFF}" = true ]; then
ruff=true
fi
fi
cat <<EOF >>"${GITHUB_OUTPUT}"
pyright=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.pyright_any_changed }}
pytest=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.pytest_any_changed }}
ruff=${{ steps.changed-files.outputs.generic_any_changed || steps.changed-files.outputs.ruff_any_changed }}
pyright=${pyright}
pytest=${pytest}
ruff=${ruff}
EOF
check-pyright:
concurrency:
group: pyright-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
name: Pyright
needs: run-conditions
if: needs.run-conditions.outputs.pyright == 'true'
@@ -89,6 +134,9 @@ jobs:
run: pyright
check-pytest:
concurrency:
group: ${{ matrix.os }}-pytest-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
@@ -151,6 +199,10 @@ jobs:
run: pytest
check-ruff:
concurrency:
group: ruff-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
name: Ruff
needs: run-conditions
if: needs.run-conditions.outputs.ruff == 'true'
+6 -1
View File
@@ -3,7 +3,12 @@
---
name: REUSE Compliance Check
on: [pull_request, push]
on:
pull_request:
push:
branches-ignore:
- translations
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
+10 -6
View File
@@ -42,13 +42,17 @@ def make_engine(connection_string: str) -> Engine:
def make_tables(engine: Engine) -> None:
logger.info("[Library] Creating DB tables...")
Base.metadata.create_all(engine)
# tag IDs < 1000 are reserved
# create tag and delete it to bump the autoincrement sequence
# TODO - find a better way
# is this the better way?
with engine.connect() as conn:
# TODO: this should instead be migrations that create the exact tables that were added in
# the respective DB versions
Base.metadata.create_all(conn)
conn.commit()
# TODO: this needs to be a migration
# tag IDs < 1000 are reserved
# create tag and delete it to bump the autoincrement sequence
# TODO - find a better way
# is this the better way?
result = conn.execute(text("SELECT SEQ FROM sqlite_sequence WHERE name='tags'"))
autoincrement_val = result.scalar()
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
File diff suppressed because it is too large Load Diff
@@ -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 "-:--"
@@ -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,
)
-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)
+6 -5
View File
@@ -395,14 +395,15 @@ class JsonMigrationModal(QObject):
# Convert JSON Library to SQLite
yield Translations["json_migration.creating_database_tables"]
self.sql_lib = SqliteLibrary()
self.temp_path: Path = (
self.json_lib.library_dir / TS_FOLDER_NAME / "migration_ts_library.sqlite"
)
temp_filename = "migration_ts_library.sqlite"
self.temp_path: Path = self.json_lib.library_dir / TS_FOLDER_NAME / temp_filename
if self.temp_path.exists():
logger.info('Temporary migration file "temp_path" already exists. Removing...')
self.temp_path.unlink()
self.sql_lib.open_sqlite_library(
self.json_lib.library_dir, is_new=True, storage_path=str(self.temp_path)
self.sql_lib.create_sqlite_library(
self.json_lib.library_dir,
in_memory=False,
sql_filename=temp_filename,
)
yield Translations.format(
"json_migration.migrating_files_entries", entries=len(self.json_lib.entries)
+2
View File
@@ -1229,6 +1229,8 @@ class ThumbRenderer(QObject):
"QuickLook/Preview.heic",
"QuickLook/Thumbnail.jpg",
"QuickLook/Thumbnail.heic",
"QuickLook/Thumbnail.webp",
"QuickLook/Icon.webp",
]
im: Image.Image | None = None
@@ -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