Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions src/tagstudio/qt/controllers/preview_thumb_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@


class PreviewThumb(PreviewThumbView):
__current_file: Path

def __init__(self, library: Library, driver: "QtDriver"):
super().__init__(library, driver)

Expand Down Expand Up @@ -114,7 +112,7 @@ def __get_video_res(self, filepath: str) -> tuple[bool, QSize]:

def display_file(self, filepath: Path) -> FileAttributeData:
"""Render a single file preview."""
self.__current_file = filepath
self._current_file = filepath

ext = filepath.suffix.lower()

Expand Down Expand Up @@ -150,21 +148,26 @@ def display_file(self, filepath: Path) -> FileAttributeData:

@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,
)
22 changes: 12 additions & 10 deletions src/tagstudio/qt/mixed/file_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import typing
from dataclasses import dataclass
from datetime import datetime as dt
from datetime import timedelta
from pathlib import Path

import structlog
Expand Down Expand Up @@ -39,6 +38,17 @@ class FileAttributeData:
duration: int | None = 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 "-:--"


class FileAttributes(QWidget):
"""The Preview Panel Widget."""

Expand Down Expand Up @@ -224,15 +234,7 @@ def add_newline(stats_label_text: str) -> str:

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)
Expand Down
30 changes: 30 additions & 0 deletions src/tagstudio/qt/views/preview_panel_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def __init__(self, library: Library, driver: "QtDriver") -> None:
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
self.__current_stats_filepath: Path | None = None

preview_section = QWidget()
preview_layout = QVBoxLayout(preview_section)
Expand Down Expand Up @@ -132,13 +134,33 @@ def __init__(self, library: Library, driver: "QtDriver") -> None:
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()

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.__current_stats_filepath:
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()

Expand All @@ -155,6 +177,8 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non
# No Items Selected
if len(selected) == 0:
self._thumb.hide_preview()
self.__current_stats = None
self.__current_stats_filepath = None
self._file_attrs.update_stats()
self._file_attrs.update_date_label()
self._containers.hide_containers()
Expand All @@ -167,9 +191,13 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non
entry: Entry = unwrap(self.lib.get_entry(entry_id))

filepath: Path = unwrap(self.lib.library_dir) / entry.path
if filepath != self.__current_stats_filepath:
self.__current_stats = None
self.__current_stats_filepath = filepath

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)
Expand All @@ -182,6 +210,8 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non
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.__current_stats_filepath = None
self._file_attrs.update_multi_selection(len(selected))
self._file_attrs.update_date_label()
self._containers.hide_containers() # TODO: Allow for mixed editing
Expand Down
52 changes: 39 additions & 13 deletions src/tagstudio/qt/views/preview_thumb_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ class PreviewThumbView(QWidget):
"""The Preview Panel Widget."""

check_ffmpeg = Signal(bool)
stats_updated = Signal(object, object)

__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:
Expand All @@ -47,6 +49,8 @@ def __init__(self, library: Library, driver: "QtDriver") -> None:
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)
Expand Down Expand Up @@ -92,6 +96,10 @@ def __init__(self, library: Library, driver: "QtDriver") -> None:
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(
Expand Down Expand Up @@ -128,6 +136,16 @@ def _button_wrapper_callback(self):
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:
Expand Down Expand Up @@ -207,7 +225,8 @@ def __switch_preview(self, preview: MediaType | None) -> None:
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),
Expand All @@ -221,17 +240,16 @@ def __render_thumb(self, filepath: Path) -> None:
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()
Expand All @@ -250,10 +268,13 @@ def _display_video(self, filepath: Path, size: QSize | None) -> FileAttributeDat
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.
Expand Down Expand Up @@ -296,14 +317,19 @@ def _display_image(self, filepath: Path):
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)

Expand Down
Loading