mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-13 09:18:04 +02:00
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
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user