mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-01 15:49:09 +00:00
refactor: split up preview_panel
This commit is contained in:
committed by
Travis Abendshien
parent
f431cf7a6b
commit
d17e279aff
@@ -65,4 +65,4 @@ class LibraryPrefs(DefaultEnum):
|
||||
IS_EXCLUDE_LIST = True
|
||||
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
|
||||
PAGE_SIZE: int = 500
|
||||
DB_VERSION: int = 2
|
||||
DB_VERSION: int = 3
|
||||
|
||||
@@ -138,6 +138,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.args = args
|
||||
self.frame_content = []
|
||||
self.filter = FilterState.show_all()
|
||||
self.frame_content: list[Entry] = []
|
||||
self.filter = FilterState().show_all()
|
||||
self.pages_count = 0
|
||||
|
||||
self.scrollbar_pos = 0
|
||||
@@ -642,7 +644,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
self.main_window.setWindowTitle(self.base_title)
|
||||
|
||||
self.selected = []
|
||||
self.selected: list[int] = []
|
||||
self.frame_content = []
|
||||
[x.set_mode(None) for x in self.item_thumbs]
|
||||
|
||||
|
||||
720
tagstudio/src/qt/widgets/preview/field_containers.py
Normal file
720
tagstudio/src/qt/widgets/preview/field_containers.py
Normal file
@@ -0,0 +1,720 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import typing
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library.alchemy.library import Library
|
||||
from src.qt.translations import Translations
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldContainers(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
tags_updated = Signal()
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
|
||||
|
||||
# self.is_connected = False
|
||||
# self.lib = library
|
||||
# self.driver: QtDriver = driver
|
||||
# self.initialized = False
|
||||
# self.is_open: bool = False
|
||||
# self.common_fields: list = []
|
||||
# self.mixed_fields: list = []
|
||||
# self.selected: list[int] = [] # New way of tracking items
|
||||
# self.containers: list[FieldContainer] = []
|
||||
|
||||
# self.panel_bg_color = (
|
||||
# Theme.COLOR_BG_DARK.value
|
||||
# if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
# else Theme.COLOR_BG_LIGHT.value
|
||||
# )
|
||||
|
||||
# self.scroll_layout = QVBoxLayout()
|
||||
# self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
# self.scroll_layout.setContentsMargins(6, 1, 6, 6)
|
||||
|
||||
# scroll_container: QWidget = QWidget()
|
||||
# scroll_container.setObjectName("entryScrollContainer")
|
||||
# scroll_container.setLayout(self.scroll_layout)
|
||||
|
||||
# info_section = QWidget()
|
||||
# info_layout = QVBoxLayout(info_section)
|
||||
# info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# info_layout.setSpacing(6)
|
||||
|
||||
# self.scroll_area = QScrollArea()
|
||||
# self.scroll_area.setObjectName("entryScrollArea")
|
||||
# self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
# self.scroll_area.setWidgetResizable(True)
|
||||
# self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
# self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
# # NOTE: I would rather have this style applied to the scroll_area
|
||||
# # background and NOT the scroll container background, so that the
|
||||
# # rounded corners are maintained when scrolling. I was unable to
|
||||
# # find the right trick to only select that particular element.
|
||||
|
||||
# self.scroll_area.setStyleSheet(
|
||||
# "QWidget#entryScrollContainer{"
|
||||
# f"background:{self.panel_bg_color};"
|
||||
# "border-radius:6px;"
|
||||
# "}"
|
||||
# )
|
||||
# self.scroll_area.setWidget(scroll_container)
|
||||
|
||||
# self.afb_container = QWidget()
|
||||
# self.afb_layout = QVBoxLayout(self.afb_container)
|
||||
# self.afb_layout.setContentsMargins(0, 12, 0, 0)
|
||||
|
||||
# self.add_field_button = QPushButtonWrapper()
|
||||
# self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
# self.add_field_button.setMinimumSize(96, 28)
|
||||
# self.add_field_button.setMaximumSize(96, 28)
|
||||
# Translations.translate_qobject(self.add_field_button, "library.field.add")
|
||||
# self.afb_layout.addWidget(self.add_field_button)
|
||||
# self.add_field_modal = AddFieldModal(self.lib)
|
||||
# self.place_add_field_button()
|
||||
|
||||
# def remove_field_prompt(self, name: str) -> str:
|
||||
# return Translations.translate_formatted("library.field.confirm_remove", name=name)
|
||||
|
||||
# def place_add_field_button(self):
|
||||
# self.scroll_layout.addWidget(self.afb_container)
|
||||
# self.scroll_layout.setAlignment(self.afb_container, Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
# if self.add_field_modal.is_connected:
|
||||
# self.add_field_modal.done.disconnect()
|
||||
# if self.add_field_button.is_connected:
|
||||
# self.add_field_button.clicked.disconnect()
|
||||
|
||||
# self.add_field_modal.done.connect(
|
||||
# lambda f: (self.add_field_to_selected(f), self.update_widgets())
|
||||
# )
|
||||
# self.add_field_modal.is_connected = True
|
||||
# self.add_field_button.clicked.connect(self.add_field_modal.show)
|
||||
|
||||
# def add_field_to_selected(self, field_list: list):
|
||||
# """Add list of entry fields to one or more selected items."""
|
||||
# logger.info("add_field_to_selected", selected=self.selected, fields=field_list)
|
||||
# for grid_idx in self.selected:
|
||||
# entry = self.driver.frame_content[grid_idx]
|
||||
# for field_item in field_list:
|
||||
# self.lib.add_entry_field_type(
|
||||
# entry.id,
|
||||
# field_id=field_item.data(Qt.ItemDataRole.UserRole),
|
||||
# )
|
||||
|
||||
# def write_container(self, index: int, field: BaseField, is_mixed: bool = False):
|
||||
# """Update/Create data for a FieldContainer.
|
||||
|
||||
# Args:
|
||||
# index(int): The container index.
|
||||
# field(BaseField): The type of field to write to.
|
||||
# is_mixed(bool): Relevant when multiple items are selected.
|
||||
|
||||
# If True, field is not present in all selected items.
|
||||
# """
|
||||
# # Remove 'Add Field' button from scroll_layout, to be re-added later.
|
||||
# self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget()
|
||||
# if len(self.containers) < (index + 1):
|
||||
# container = FieldContainer()
|
||||
# self.containers.append(container)
|
||||
# self.scroll_layout.addWidget(container)
|
||||
# else:
|
||||
# container = self.containers[index]
|
||||
|
||||
# # if isinstance(field, TagBoxField):
|
||||
# # container.set_title(field.type.name)
|
||||
# # container.set_inline(False)
|
||||
# # title = f"{field.type.name} (Tag Box)"
|
||||
# #
|
||||
# # if not is_mixed:
|
||||
# # inner_container = container.get_inner_widget()
|
||||
# # if isinstance(inner_container, TagBoxWidget):
|
||||
# # inner_container.set_field(field)
|
||||
# # inner_container.set_tags(list(field.tags))
|
||||
# #
|
||||
# # try:
|
||||
# # inner_container.updated.disconnect()
|
||||
# # except RuntimeError:
|
||||
# # logger.error("Failed to disconnect inner_container.updated")
|
||||
# #
|
||||
# # else:
|
||||
# # inner_container = TagBoxWidget(
|
||||
# # field,
|
||||
# # title,
|
||||
# # self.driver,
|
||||
# # )
|
||||
# #
|
||||
# # container.set_inner_widget(inner_container)
|
||||
# #
|
||||
# # inner_container.updated.connect(
|
||||
# # lambda: (
|
||||
# # self.write_container(index, field),
|
||||
# # self.update_widgets(),
|
||||
# # )
|
||||
# # )
|
||||
# # # NOTE: Tag Boxes have no Edit Button
|
||||
# # (But will when you can convert field types)
|
||||
# # container.set_remove_callback(
|
||||
# # lambda: self.remove_message_box(
|
||||
# # prompt=self.remove_field_prompt(field.type.name),
|
||||
# # callback=lambda: (
|
||||
# # self.remove_field(field),
|
||||
# # self.update_selected_entry(self.driver),
|
||||
# # # reload entry and its fields
|
||||
# # self.update_widgets(),
|
||||
# # ),
|
||||
# # )
|
||||
# # )
|
||||
# # else:
|
||||
# # text = "<i>Mixed Data</i>"
|
||||
# # title = f"{field.type.name} (Wacky Tag Box)"
|
||||
# # inner_container = TextWidget(title, text)
|
||||
# # container.set_inner_widget(inner_container)
|
||||
# #
|
||||
# # self.tags_updated.emit()
|
||||
# # # self.dynamic_widgets.append(inner_container)
|
||||
# # elif field.type.type == FieldTypeEnum.TEXT_LINE:
|
||||
# if field.type.type == FieldTypeEnum.TEXT_LINE:
|
||||
# container.set_title(field.type.name)
|
||||
# container.set_inline(False)
|
||||
|
||||
# # Normalize line endings in any text content.
|
||||
# if not is_mixed:
|
||||
# assert isinstance(field.value, (str, type(None)))
|
||||
# text = field.value or ""
|
||||
# else:
|
||||
# text = "<i>Mixed Data</i>"
|
||||
|
||||
# title = f"{field.type.name} ({field.type.type.value})"
|
||||
# inner_container = TextWidget(title, text)
|
||||
# container.set_inner_widget(inner_container)
|
||||
# if not is_mixed:
|
||||
# modal = PanelModal(
|
||||
# EditTextLine(field.value),
|
||||
# title=title,
|
||||
# window_title=f"Edit {field.type.type.value}",
|
||||
# save_callback=(
|
||||
# lambda content: (
|
||||
# self.update_field(field, content),
|
||||
# self.update_widgets(),
|
||||
# )
|
||||
# ),
|
||||
# )
|
||||
# if "pytest" in sys.modules:
|
||||
# # for better testability
|
||||
# container.modal = modal # type: ignore
|
||||
|
||||
# container.set_edit_callback(modal.show)
|
||||
# container.set_remove_callback(
|
||||
# lambda: self.remove_message_box(
|
||||
# prompt=self.remove_field_prompt(field.type.type.value),
|
||||
# callback=lambda: (
|
||||
# self.remove_field(field),
|
||||
# self.update_widgets(),
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
|
||||
# elif field.type.type == FieldTypeEnum.TEXT_BOX:
|
||||
# container.set_title(field.type.name)
|
||||
# # container.set_editable(True)
|
||||
# container.set_inline(False)
|
||||
# # Normalize line endings in any text content.
|
||||
# if not is_mixed:
|
||||
# assert isinstance(field.value, (str, type(None)))
|
||||
# text = (field.value or "").replace("\r", "\n")
|
||||
# else:
|
||||
# text = "<i>Mixed Data</i>"
|
||||
# title = f"{field.type.name} (Text Box)"
|
||||
# inner_container = TextWidget(title, text)
|
||||
# container.set_inner_widget(inner_container)
|
||||
# if not is_mixed:
|
||||
# modal = PanelModal(
|
||||
# EditTextBox(field.value),
|
||||
# title=title,
|
||||
# window_title=f"Edit {field.type.name}",
|
||||
# save_callback=(
|
||||
# lambda content: (
|
||||
# self.update_field(field, content),
|
||||
# self.update_widgets(),
|
||||
# )
|
||||
# ),
|
||||
# )
|
||||
# container.set_edit_callback(modal.show)
|
||||
# container.set_remove_callback(
|
||||
# lambda: self.remove_message_box(
|
||||
# prompt=self.remove_field_prompt(field.type.name),
|
||||
# callback=lambda: (
|
||||
# self.remove_field(field),
|
||||
# self.update_widgets(),
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
|
||||
# elif field.type.type == FieldTypeEnum.DATETIME:
|
||||
# if not is_mixed:
|
||||
# try:
|
||||
# container.set_title(field.type.name)
|
||||
# # container.set_editable(False)
|
||||
# container.set_inline(False)
|
||||
# # TODO: Localize this and/or add preferences.
|
||||
# date = dt.strptime(field.value, "%Y-%m-%d %H:%M:%S")
|
||||
# title = f"{field.type.name} (Date)"
|
||||
# inner_container = TextWidget(title, date.strftime("%D - %r"))
|
||||
# container.set_inner_widget(inner_container)
|
||||
# except Exception:
|
||||
# container.set_title(field.type.name)
|
||||
# # container.set_editable(False)
|
||||
# container.set_inline(False)
|
||||
# title = f"{field.type.name} (Date) (Unknown Format)"
|
||||
# inner_container = TextWidget(title, str(field.value))
|
||||
# container.set_inner_widget(inner_container)
|
||||
|
||||
# container.set_remove_callback(
|
||||
# lambda: self.remove_message_box(
|
||||
# prompt=self.remove_field_prompt(field.type.name),
|
||||
# callback=lambda: (
|
||||
# self.remove_field(field),
|
||||
# self.update_widgets(),
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# else:
|
||||
# text = "<i>Mixed Data</i>"
|
||||
# title = f"{field.type.name} (Wacky Date)"
|
||||
# inner_container = TextWidget(title, text)
|
||||
# container.set_inner_widget(inner_container)
|
||||
# else:
|
||||
# logger.warning("write_container - unknown field", field=field)
|
||||
# container.set_title(field.type.name)
|
||||
# container.set_inline(False)
|
||||
# title = f"{field.type.name} (Unknown Field Type)"
|
||||
# inner_container = TextWidget(title, field.type.name)
|
||||
# container.set_inner_widget(inner_container)
|
||||
# container.set_remove_callback(
|
||||
# lambda: self.remove_message_box(
|
||||
# prompt=self.remove_field_prompt(field.type.name),
|
||||
# callback=lambda: (
|
||||
# self.remove_field(field),
|
||||
# self.update_widgets(),
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
|
||||
# container.edit_button.setHidden(True)
|
||||
# container.setHidden(False)
|
||||
# self.place_add_field_button()
|
||||
|
||||
# def remove_field(self, field: BaseField):
|
||||
# """Remove a field from all selected Entries."""
|
||||
# logger.info("removing field", field=field, selected=self.selected)
|
||||
# entry_ids = []
|
||||
|
||||
# for grid_idx in self.selected:
|
||||
# entry = self.driver.frame_content[grid_idx]
|
||||
# entry_ids.append(entry.id)
|
||||
|
||||
# self.lib.remove_entry_field(field, entry_ids)
|
||||
|
||||
# # # if the field is meta tags, update the badges
|
||||
# # if field.type_key == _FieldID.TAGS_META.value:
|
||||
# # self.driver.update_badges(self.selected)
|
||||
|
||||
# def update_field(self, field: BaseField, content: str) -> None:
|
||||
# """Update a field in all selected Entries, given a field object."""
|
||||
# assert isinstance(
|
||||
# field,
|
||||
# (TextField, DatetimeField), # , TagBoxField)
|
||||
# ), f"instance: {type(field)}"
|
||||
|
||||
# entry_ids = []
|
||||
# for grid_idx in self.selected:
|
||||
# entry = self.driver.frame_content[grid_idx]
|
||||
# entry_ids.append(entry.id)
|
||||
|
||||
# assert entry_ids, "No entries selected"
|
||||
# self.lib.update_entry_field(
|
||||
# entry_ids,
|
||||
# field,
|
||||
# content,
|
||||
# )
|
||||
|
||||
# def remove_message_box(self, prompt: str, callback: Callable) -> None:
|
||||
# remove_mb = QMessageBox()
|
||||
# remove_mb.setText(prompt)
|
||||
# remove_mb.setWindowTitle("Remove Field")
|
||||
# remove_mb.setIcon(QMessageBox.Icon.Warning)
|
||||
# cancel_button = remove_mb.addButton(
|
||||
# Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole
|
||||
# )
|
||||
# remove_mb.addButton("&Remove", QMessageBox.ButtonRole.RejectRole)
|
||||
# # remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel)
|
||||
# remove_mb.setDefaultButton(cancel_button)
|
||||
# remove_mb.setEscapeButton(cancel_button)
|
||||
# result = remove_mb.exec_()
|
||||
# # logging.info(result)
|
||||
# if result == 3: # TODO - what is this magic number?
|
||||
# callback()
|
||||
|
||||
# def set_tags_updated_slot(self, slot: object):
|
||||
# """Replacement for tag_callback."""
|
||||
# if self.is_connected:
|
||||
# self.tags_updated.disconnect()
|
||||
|
||||
# logger.info("[UPDATE CONTAINER] Setting tags updated slot")
|
||||
# self.tags_updated.connect(slot)
|
||||
# self.is_connected = True
|
||||
|
||||
# def update_widgets(self):
|
||||
# """Render the panel widgets with the newest data from the Library."""
|
||||
# logger.info("update_widgets", selected=self.driver.selected)
|
||||
# self.is_open = True
|
||||
# # self.tag_callback = tag_callback if tag_callback else None
|
||||
# window_title = ""
|
||||
|
||||
# # # update list of libraries
|
||||
# # self.fill_libs_widget(self.libs_layout)
|
||||
|
||||
# if not self.driver.selected:
|
||||
# if self.selected or not self.initialized:
|
||||
# self.file_label.setText("<i>No Items Selected</i>")
|
||||
# self.file_label.set_file_path("")
|
||||
# self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
# self.dimensions_label.setText("")
|
||||
# self.update_date_label()
|
||||
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
||||
# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
# ratio = self.devicePixelRatio()
|
||||
# self.thumb_renderer.render(
|
||||
# time.time(),
|
||||
# "",
|
||||
# (512, 512),
|
||||
# ratio,
|
||||
# is_loading=True,
|
||||
# update_on_ratio_change=True,
|
||||
# )
|
||||
# if self.preview_img.is_connected:
|
||||
# self.preview_img.clicked.disconnect()
|
||||
# for c in self.containers:
|
||||
# c.setHidden(True)
|
||||
# self.preview_img.show()
|
||||
# self.preview_vid.stop()
|
||||
# self.preview_vid.hide()
|
||||
# self.media_player.hide()
|
||||
# self.media_player.stop()
|
||||
# self.preview_gif.hide()
|
||||
# self.selected = list(self.driver.selected)
|
||||
# self.add_field_button.setHidden(True)
|
||||
|
||||
# # common code
|
||||
# self.initialized = True
|
||||
# self.setWindowTitle(window_title)
|
||||
# self.show()
|
||||
# return True
|
||||
|
||||
# # reload entry and fill it into the grid again
|
||||
# # TODO - do this more granular
|
||||
# # TODO - Entry reload is maybe not necessary
|
||||
# for grid_idx in self.driver.selected:
|
||||
# entry = self.driver.frame_content[grid_idx]
|
||||
# results = self.lib.search_library(FilterState(id=entry.id))
|
||||
# logger.info(
|
||||
# "found item",
|
||||
# entries=len(results.items),
|
||||
# grid_idx=grid_idx,
|
||||
# lookup_id=entry.id,
|
||||
# )
|
||||
# self.driver.frame_content[grid_idx] = results[0]
|
||||
|
||||
# if len(self.driver.selected) == 1:
|
||||
# # 1 Selected Entry
|
||||
# selected_idx = self.driver.selected[0]
|
||||
# item = self.driver.frame_content[selected_idx]
|
||||
|
||||
# self.preview_img.show()
|
||||
# self.preview_vid.stop()
|
||||
# self.preview_vid.hide()
|
||||
# self.media_player.stop()
|
||||
# self.media_player.hide()
|
||||
# self.preview_gif.hide()
|
||||
|
||||
# # If a new selection is made, update the thumbnail and filepath.
|
||||
# if not self.selected or self.selected != self.driver.selected:
|
||||
# filepath = self.lib.library_dir / item.path
|
||||
# self.file_label.set_file_path(filepath)
|
||||
# ratio = self.devicePixelRatio()
|
||||
# self.thumb_renderer.render(
|
||||
# time.time(),
|
||||
# filepath,
|
||||
# (512, 512),
|
||||
# ratio,
|
||||
# update_on_ratio_change=True,
|
||||
# )
|
||||
# file_str: str = ""
|
||||
# separator: str = f"<a style='color: #777777'><b>{os.path.sep}</a>" # Gray
|
||||
# for i, part in enumerate(filepath.parts):
|
||||
# part_ = part.strip(os.path.sep)
|
||||
# if i != len(filepath.parts) - 1:
|
||||
# file_str += f"{"\u200b".join(part_)}{separator}</b>"
|
||||
# else:
|
||||
# file_str += f"<br><b>{"\u200b".join(part_)}</b>"
|
||||
# self.file_label.setText(file_str)
|
||||
# self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
# self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# self.opener = FileOpenerHelper(filepath)
|
||||
# self.open_file_action.triggered.connect(self.opener.open_file)
|
||||
# self.open_explorer_action.triggered.connect(self.opener.open_explorer)
|
||||
|
||||
# # TODO: Do this all somewhere else, this is just here temporarily.
|
||||
# ext: str = filepath.suffix.lower()
|
||||
# try:
|
||||
# if MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# if self.preview_gif.movie():
|
||||
# self.preview_gif.movie().stop()
|
||||
# self.gif_buffer.close()
|
||||
|
||||
# image: Image.Image = Image.open(filepath)
|
||||
# anim_image: Image.Image = image
|
||||
# image_bytes_io: io.BytesIO = io.BytesIO()
|
||||
# anim_image.save(
|
||||
# image_bytes_io,
|
||||
# "GIF",
|
||||
# lossless=True,
|
||||
# save_all=True,
|
||||
# loop=0,
|
||||
# disposal=2,
|
||||
# )
|
||||
# image_bytes_io.seek(0)
|
||||
# ba: bytes = image_bytes_io.read()
|
||||
|
||||
# self.gif_buffer.setData(ba)
|
||||
# movie = QMovie(self.gif_buffer, QByteArray())
|
||||
# self.preview_gif.setMovie(movie)
|
||||
# movie.start()
|
||||
|
||||
# self.resizeEvent(
|
||||
# QResizeEvent(
|
||||
# QSize(image.width, image.height),
|
||||
# QSize(image.width, image.height),
|
||||
# )
|
||||
# )
|
||||
# self.preview_img.hide()
|
||||
# self.preview_vid.hide()
|
||||
# self.preview_gif.show()
|
||||
|
||||
# image = None
|
||||
# if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RASTER_TYPES):
|
||||
# image = Image.open(str(filepath))
|
||||
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES):
|
||||
# try:
|
||||
# with rawpy.imread(str(filepath)) as raw:
|
||||
# rgb = raw.postprocess()
|
||||
# image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
|
||||
# except (
|
||||
# rawpy._rawpy.LibRawIOError,
|
||||
# rawpy._rawpy.LibRawFileUnsupportedError,
|
||||
# ):
|
||||
# pass
|
||||
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.AUDIO_TYPES):
|
||||
# self.media_player.show()
|
||||
# self.media_player.play(filepath)
|
||||
# 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()
|
||||
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# image = Image.fromarray(frame)
|
||||
# if success:
|
||||
# self.preview_img.hide()
|
||||
# self.preview_vid.play(filepath, QSize(image.width, image.height))
|
||||
# self.resizeEvent(
|
||||
# QResizeEvent(
|
||||
# QSize(image.width, image.height),
|
||||
# QSize(image.width, image.height),
|
||||
# )
|
||||
# )
|
||||
# self.preview_vid.show()
|
||||
|
||||
# # Stats for specific file types are displayed here.
|
||||
# if image and (
|
||||
# MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.IMAGE_RASTER_TYPES, mime_fallback=True
|
||||
# )
|
||||
# or MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
|
||||
# )
|
||||
# or MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
|
||||
# )
|
||||
# ):
|
||||
# self.dimensions_label.setText(
|
||||
# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
|
||||
# f"{image.width} x {image.height} px"
|
||||
# )
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.FONT_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# try:
|
||||
# font = ImageFont.truetype(filepath)
|
||||
# self.dimensions_label.setText(
|
||||
# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
|
||||
# f"{font.getname()[0]} ({font.getname()[1]}) "
|
||||
# )
|
||||
# except OSError:
|
||||
# self.dimensions_label.setText(
|
||||
# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
# )
|
||||
# logger.info(
|
||||
# f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}"
|
||||
# )
|
||||
# else:
|
||||
# self.dimensions_label.setText(f"{ext.upper()[1:]}")
|
||||
# self.dimensions_label.setText(
|
||||
# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
# )
|
||||
# self.update_date_label(filepath)
|
||||
|
||||
# if not filepath.is_file():
|
||||
# raise FileNotFoundError
|
||||
|
||||
# except (FileNotFoundError, cv2.error) as e:
|
||||
# self.dimensions_label.setText(f"{ext.upper()[1:]}")
|
||||
# logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
# self.update_date_label()
|
||||
# except (
|
||||
# UnidentifiedImageError,
|
||||
# DecompressionBombError,
|
||||
# ) as e:
|
||||
# self.dimensions_label.setText(
|
||||
# f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
# )
|
||||
# logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
# self.update_date_label(filepath)
|
||||
|
||||
# if self.preview_img.is_connected:
|
||||
# self.preview_img.clicked.disconnect()
|
||||
# self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth))
|
||||
# self.preview_img.is_connected = True
|
||||
|
||||
# self.selected = self.driver.selected
|
||||
# logger.info(
|
||||
# "rendering item fields",
|
||||
# item=item.id,
|
||||
# fields=[x.type_key for x in item.fields],
|
||||
# )
|
||||
# for idx, field in enumerate(item.fields):
|
||||
# self.write_container(idx, field)
|
||||
|
||||
# # Hide leftover containers
|
||||
# if len(self.containers) > len(item.fields):
|
||||
# for i, c in enumerate(self.containers):
|
||||
# if i > (len(item.fields) - 1):
|
||||
# c.setHidden(True)
|
||||
|
||||
# self.add_field_button.setHidden(False)
|
||||
|
||||
# # Multiple Selected Items
|
||||
# elif len(self.driver.selected) > 1:
|
||||
# self.preview_img.show()
|
||||
# self.preview_gif.hide()
|
||||
# self.preview_vid.stop()
|
||||
# self.preview_vid.hide()
|
||||
# self.media_player.stop()
|
||||
# self.media_player.hide()
|
||||
# self.update_date_label()
|
||||
# if self.selected != self.driver.selected:
|
||||
# self.file_label.setText(f"<b>{len(self.driver.selected)}</b> Items Selected")
|
||||
# self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
# self.file_label.set_file_path("")
|
||||
# self.dimensions_label.setText("")
|
||||
|
||||
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
||||
# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
# ratio = self.devicePixelRatio()
|
||||
# self.thumb_renderer.render(
|
||||
# time.time(),
|
||||
# "",
|
||||
# (512, 512),
|
||||
# ratio,
|
||||
# is_loading=True,
|
||||
# update_on_ratio_change=True,
|
||||
# )
|
||||
# if self.preview_img.is_connected:
|
||||
# self.preview_img.clicked.disconnect()
|
||||
# self.preview_img.is_connected = False
|
||||
|
||||
# # fill shared fields from first item
|
||||
# first_item = self.driver.frame_content[self.driver.selected[0]]
|
||||
# common_fields = [f for f in first_item.fields]
|
||||
# mixed_fields = []
|
||||
|
||||
# # iterate through other items
|
||||
# for grid_idx in self.driver.selected[1:]:
|
||||
# item = self.driver.frame_content[grid_idx]
|
||||
# item_field_types = {f.type_key for f in item.fields}
|
||||
# for f in common_fields[:]:
|
||||
# if f.type_key not in item_field_types:
|
||||
# common_fields.remove(f)
|
||||
# mixed_fields.append(f)
|
||||
|
||||
# self.common_fields = common_fields
|
||||
# self.mixed_fields = sorted(mixed_fields, key=lambda x: x.type.position)
|
||||
|
||||
# self.selected = list(self.driver.selected)
|
||||
# logger.info(
|
||||
# "update_widgets common_fields",
|
||||
# common_fields=self.common_fields,
|
||||
# )
|
||||
# for i, f in enumerate(self.common_fields):
|
||||
# self.write_container(i, f)
|
||||
|
||||
# logger.info(
|
||||
# "update_widgets mixed_fields",
|
||||
# mixed_fields=self.mixed_fields,
|
||||
# start=len(self.common_fields),
|
||||
# )
|
||||
# for i, f in enumerate(self.mixed_fields, start=len(self.common_fields)):
|
||||
# self.write_container(i, f, is_mixed=True)
|
||||
|
||||
# # Hide leftover containers
|
||||
# if len(self.containers) > len(self.common_fields) + len(self.mixed_fields):
|
||||
# for i, c in enumerate(self.containers):
|
||||
# if i > (len(self.common_fields) + len(self.mixed_fields) - 1):
|
||||
# c.setHidden(True)
|
||||
|
||||
# self.add_field_button.setHidden(False)
|
||||
|
||||
# self.initialized = True
|
||||
|
||||
# self.setWindowTitle(window_title)
|
||||
# self.show()
|
||||
# return True
|
||||
232
tagstudio/src/qt/widgets/preview/file_attributes.py
Normal file
232
tagstudio/src/qt/widgets/preview/file_attributes.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import os
|
||||
import platform
|
||||
import typing
|
||||
from datetime import datetime as dt
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import structlog
|
||||
from humanfriendly import format_size
|
||||
from PIL import Image, ImageFont, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.enums import Theme
|
||||
from src.core.library.alchemy.library import Library
|
||||
from src.core.media_types import MediaCategories
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel
|
||||
from src.qt.translations import Translations
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FileAttributes(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
tags_updated = Signal()
|
||||
|
||||
label_bg_color = (
|
||||
Theme.COLOR_BG_DARK.value
|
||||
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
else Theme.COLOR_DARK_LABEL.value
|
||||
)
|
||||
|
||||
# panel_bg_color = (
|
||||
# Theme.COLOR_BG_DARK.value
|
||||
# if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
# else Theme.COLOR_BG_LIGHT.value
|
||||
# )
|
||||
|
||||
file_label_style = "font-size: 12px"
|
||||
properties_style = (
|
||||
f"background-color:{label_bg_color};"
|
||||
"color:#FFFFFF;"
|
||||
"font-family:Oxanium;"
|
||||
"font-weight:bold;"
|
||||
"font-size:12px;"
|
||||
"border-radius:3px;"
|
||||
"padding-top: 4px;"
|
||||
"padding-right: 1px;"
|
||||
"padding-bottom: 1px;"
|
||||
"padding-left: 1px;"
|
||||
)
|
||||
date_style = "font-size:12px;"
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
# self.is_connected = False
|
||||
# self.lib = library
|
||||
# self.driver: QtDriver = driver
|
||||
# self.initialized = False
|
||||
# self.is_open: bool = False
|
||||
|
||||
root_layout = QVBoxLayout(self)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.setSpacing(6)
|
||||
|
||||
self.file_label = FileOpenerLabel()
|
||||
self.file_label.setObjectName("filenameLabel")
|
||||
self.file_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.file_label.setWordWrap(True)
|
||||
self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self.file_label.setStyleSheet(FileAttributes.file_label_style)
|
||||
|
||||
self.date_created_label = QLabel()
|
||||
self.date_created_label.setObjectName("dateCreatedLabel")
|
||||
self.date_created_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.date_created_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.date_created_label.setStyleSheet(FileAttributes.date_style)
|
||||
|
||||
self.date_modified_label = QLabel()
|
||||
self.date_modified_label.setObjectName("dateModifiedLabel")
|
||||
self.date_modified_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.date_modified_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.date_modified_label.setStyleSheet(FileAttributes.date_style)
|
||||
|
||||
self.dimensions_label = QLabel()
|
||||
self.dimensions_label.setObjectName("dimensionsLabel")
|
||||
self.dimensions_label.setWordWrap(True)
|
||||
self.dimensions_label.setStyleSheet(FileAttributes.properties_style)
|
||||
|
||||
self.date_container = QWidget()
|
||||
date_layout = QVBoxLayout(self.date_container)
|
||||
date_layout.setContentsMargins(0, 2, 0, 0)
|
||||
date_layout.setSpacing(0)
|
||||
date_layout.addWidget(self.date_created_label)
|
||||
date_layout.addWidget(self.date_modified_label)
|
||||
|
||||
root_layout.addWidget(self.file_label)
|
||||
root_layout.addWidget(self.date_container)
|
||||
root_layout.addWidget(self.dimensions_label)
|
||||
|
||||
def update_date_label(self, filepath: Path | None = None) -> None:
|
||||
"""Update the "Date Created" and "Date Modified" file property labels."""
|
||||
logger.info(filepath)
|
||||
if filepath and filepath.is_file():
|
||||
created: dt = None
|
||||
if platform.system() == "Windows" or platform.system() == "Darwin":
|
||||
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
|
||||
else:
|
||||
created = dt.fromtimestamp(filepath.stat().st_ctime)
|
||||
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
|
||||
self.date_created_label.setText(
|
||||
f"<b>Date Created:</b> {dt.strftime(created, "%a, %x, %X")}"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>Date Modified:</b> {dt.strftime(modified, "%a, %x, %X")}"
|
||||
)
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
elif filepath:
|
||||
self.date_created_label.setText("<b>Date Created:</b> <i>N/A</i>")
|
||||
self.date_modified_label.setText("<b>Date Modified:</b> <i>N/A</i>")
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
else:
|
||||
self.date_created_label.setHidden(True)
|
||||
self.date_modified_label.setHidden(True)
|
||||
|
||||
def update_stats(self, filepath: Path | None = None):
|
||||
"""Render the panel widgets with the newest data from the Library."""
|
||||
logger.info("update_stats", selected=filepath)
|
||||
|
||||
if not filepath:
|
||||
self.file_label.setText("<i>No Items Selected</i>")
|
||||
self.file_label.set_file_path("")
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.dimensions_label.setText("")
|
||||
else:
|
||||
self.file_label.set_file_path(filepath)
|
||||
|
||||
file_str: str = ""
|
||||
separator: str = f"<a style='color: #777777'><b>{os.path.sep}</a>" # Gray
|
||||
for i, part in enumerate(filepath.parts):
|
||||
part_ = part.strip(os.path.sep)
|
||||
if i != len(filepath.parts) - 1:
|
||||
file_str += f"{"\u200b".join(part_)}{separator}</b>"
|
||||
else:
|
||||
file_str += f"<br><b>{"\u200b".join(part_)}</b>"
|
||||
self.file_label.setText(file_str)
|
||||
self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
self.opener = FileOpenerHelper(filepath)
|
||||
# self.open_file_action = QAction(self)
|
||||
# Translations.translate_qobject(self.open_file_action, "file.open_file")
|
||||
# self.open_explorer_action = QAction(PlatformStrings.open_file_str, self)
|
||||
|
||||
# TODO: Do this all somewhere else, this is just here temporarily.
|
||||
ext: str = filepath.suffix.lower()
|
||||
try:
|
||||
image: Image.Image = Image.open(filepath)
|
||||
# Stats for specific file types are displayed here.
|
||||
if image and (
|
||||
MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_RASTER_TYPES, mime_fallback=True
|
||||
)
|
||||
or MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
|
||||
)
|
||||
or MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
|
||||
)
|
||||
):
|
||||
self.dimensions_label.setText(
|
||||
f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
|
||||
f"{image.width} x {image.height} px"
|
||||
)
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.FONT_TYPES, mime_fallback=True
|
||||
):
|
||||
try:
|
||||
font = ImageFont.truetype(filepath)
|
||||
self.dimensions_label.setText(
|
||||
f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}\n"
|
||||
f"{font.getname()[0]} ({font.getname()[1]}) "
|
||||
)
|
||||
except OSError:
|
||||
self.dimensions_label.setText(
|
||||
f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
)
|
||||
logger.info(f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}")
|
||||
else:
|
||||
self.dimensions_label.setText(f"{ext.upper()[1:]}")
|
||||
self.dimensions_label.setText(
|
||||
f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
)
|
||||
self.update_date_label(filepath)
|
||||
|
||||
if not filepath.is_file():
|
||||
raise FileNotFoundError
|
||||
|
||||
except (FileNotFoundError, cv2.error) as e:
|
||||
self.dimensions_label.setText(f"{ext.upper()[1:]}")
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
self.update_date_label()
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
DecompressionBombError, # noqa: F821
|
||||
) as e:
|
||||
self.dimensions_label.setText(
|
||||
f"{ext.upper()[1:]} • {format_size(filepath.stat().st_size)}"
|
||||
)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
self.update_date_label(filepath)
|
||||
|
||||
def update_multi_selection(self, count: int):
|
||||
# Multiple Selected Items
|
||||
self.file_label.setText(f"<b>{count}</b> Items Selected")
|
||||
self.file_label.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.file_label.set_file_path("")
|
||||
self.dimensions_label.setText("")
|
||||
300
tagstudio/src/qt/widgets/preview/preview_thumb.py
Normal file
300
tagstudio/src/qt/widgets/preview/preview_thumb.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import io
|
||||
import time
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt, Signal
|
||||
from PySide6.QtGui import QMovie, QResizeEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library.alchemy.library import Library
|
||||
from src.core.library.alchemy.models import Entry
|
||||
from src.core.media_types import MediaCategories
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper, open_file
|
||||
from src.qt.helpers.file_tester import is_readable_video
|
||||
from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from src.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle
|
||||
from src.qt.translations import Translations
|
||||
from src.qt.widgets.media_player import MediaPlayer
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
from src.qt.widgets.video_player import VideoPlayer
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class PreviewThumb(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
tags_updated = Signal()
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
|
||||
self.is_connected = False
|
||||
self.lib = library
|
||||
self.driver: QtDriver = driver
|
||||
|
||||
self.img_button_size: tuple[int, int] = (266, 266)
|
||||
self.image_ratio: float = 1.0
|
||||
|
||||
# self.panel_bg_color = (
|
||||
# Theme.COLOR_BG_DARK.value
|
||||
# if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
|
||||
# else Theme.COLOR_BG_LIGHT.value
|
||||
# )
|
||||
|
||||
self.image_container = QWidget()
|
||||
image_layout = QHBoxLayout(self.image_container)
|
||||
image_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# self.open_file_action = QAction("Open file", self)
|
||||
# self.open_explorer_action = QAction(PlatformStrings.open_file_str, self)
|
||||
|
||||
self.preview_img = QPushButtonWrapper()
|
||||
self.preview_img.setMinimumSize(*self.img_button_size)
|
||||
self.preview_img.setFlat(True)
|
||||
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
# self.preview_img.addAction(self.open_file_action)
|
||||
# self.preview_img.addAction(self.open_explorer_action)
|
||||
|
||||
self.preview_gif = QLabel()
|
||||
self.preview_gif.setMinimumSize(*self.img_button_size)
|
||||
self.preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.preview_gif.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
# self.preview_gif.addAction(self.open_file_action)
|
||||
# self.preview_gif.addAction(self.open_explorer_action)
|
||||
self.preview_gif.hide()
|
||||
self.gif_buffer: QBuffer = QBuffer()
|
||||
|
||||
self.preview_vid = VideoPlayer(driver)
|
||||
self.preview_vid.hide()
|
||||
self.thumb_renderer = ThumbRenderer()
|
||||
self.thumb_renderer.updated.connect(lambda ts, i, s: (self.preview_img.setIcon(i)))
|
||||
self.thumb_renderer.updated_ratio.connect(
|
||||
lambda ratio: (
|
||||
self.set_image_ratio(ratio),
|
||||
self.update_image_size(
|
||||
(
|
||||
self.image_container.size().width(),
|
||||
self.image_container.size().height(),
|
||||
),
|
||||
ratio,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.media_player = MediaPlayer(driver)
|
||||
self.media_player.hide()
|
||||
|
||||
image_layout.addWidget(self.preview_img)
|
||||
image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter)
|
||||
image_layout.addWidget(self.preview_gif)
|
||||
image_layout.setAlignment(self.preview_gif, Qt.AlignmentFlag.AlignCenter)
|
||||
image_layout.addWidget(self.preview_vid)
|
||||
image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_container.setMinimumSize(*self.img_button_size)
|
||||
|
||||
def set_image_ratio(self, ratio: float):
|
||||
self.image_ratio = ratio
|
||||
|
||||
def update_image_size(self, size: tuple[int, int], ratio: float = None):
|
||||
if ratio:
|
||||
self.set_image_ratio(ratio)
|
||||
|
||||
adj_width: float = size[0]
|
||||
adj_height: float = size[1]
|
||||
# Landscape
|
||||
if self.image_ratio > 1:
|
||||
adj_height = size[0] * (1 / self.image_ratio)
|
||||
# Portrait
|
||||
elif self.image_ratio <= 1:
|
||||
adj_width = size[1] * self.image_ratio
|
||||
|
||||
if adj_width > size[0]:
|
||||
adj_height = adj_height * (size[0] / adj_width)
|
||||
adj_width = size[0]
|
||||
elif adj_height > size[1]:
|
||||
adj_width = adj_width * (size[1] / adj_height)
|
||||
adj_height = size[1]
|
||||
|
||||
adj_size = QSize(int(adj_width), int(adj_height))
|
||||
self.img_button_size = (int(adj_width), int(adj_height))
|
||||
self.preview_img.setMaximumSize(adj_size)
|
||||
self.preview_img.setIconSize(adj_size)
|
||||
self.preview_vid.resize_video(adj_size)
|
||||
self.preview_vid.setMaximumSize(adj_size)
|
||||
self.preview_vid.setMinimumSize(adj_size)
|
||||
self.preview_gif.setMaximumSize(adj_size)
|
||||
self.preview_gif.setMinimumSize(adj_size)
|
||||
proxy_style = RoundedPixmapStyle(radius=8)
|
||||
self.preview_gif.setStyle(proxy_style)
|
||||
self.preview_vid.setStyle(proxy_style)
|
||||
m = self.preview_gif.movie()
|
||||
if m:
|
||||
m.setScaledSize(adj_size)
|
||||
|
||||
def get_preview_size(self) -> tuple[int, int]:
|
||||
return (
|
||||
self.image_container.size().width(),
|
||||
self.image_container.size().height(),
|
||||
)
|
||||
|
||||
def update_preview(self, entry: Entry, filepath: Path) -> dict:
|
||||
"""Render a single file preview."""
|
||||
# self.tag_callback = tag_callback if tag_callback else None
|
||||
|
||||
# # update list of libraries
|
||||
# self.fill_libs_widget(self.libs_layout)
|
||||
|
||||
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
||||
# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
|
||||
# ratio = self.devicePixelRatio()
|
||||
# self.thumb_renderer.render(
|
||||
# time.time(),
|
||||
# "",
|
||||
# (512, 512),
|
||||
# ratio,
|
||||
# is_loading=True,
|
||||
# update_on_ratio_change=True,
|
||||
# )
|
||||
# if self.preview_img.is_connected:
|
||||
# self.preview_img.clicked.disconnect()
|
||||
# self.preview_img.show()
|
||||
# self.preview_vid.stop()
|
||||
# self.preview_vid.hide()
|
||||
# self.media_player.hide()
|
||||
# self.media_player.stop()
|
||||
# self.preview_gif.hide()
|
||||
# self.selected = list(self.driver.selected)
|
||||
# self.add_field_button.setHidden(True)
|
||||
|
||||
# reload entry and fill it into the grid again
|
||||
# 1 Selected Entry
|
||||
# selected_idx = self.driver.selected[0]
|
||||
# item = self.driver.frame_content[selected_idx]
|
||||
|
||||
self.preview_img.show()
|
||||
self.preview_vid.stop()
|
||||
self.preview_vid.hide()
|
||||
self.media_player.stop()
|
||||
self.media_player.hide()
|
||||
self.preview_gif.hide()
|
||||
|
||||
# If a new selection is made, update the thumbnail and filepath.
|
||||
ratio = self.devicePixelRatio()
|
||||
self.thumb_renderer.render(
|
||||
time.time(),
|
||||
filepath,
|
||||
(512, 512),
|
||||
ratio,
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
|
||||
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
self.opener = FileOpenerHelper(filepath)
|
||||
# self.open_file_action.triggered.connect(self.opener.open_file)
|
||||
# self.open_explorer_action.triggered.connect(self.opener.open_explorer)
|
||||
|
||||
# TODO: Do this all somewhere else, this is just here temporarily.
|
||||
ext: str = filepath.suffix.lower()
|
||||
try:
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
|
||||
):
|
||||
if self.preview_gif.movie():
|
||||
self.preview_gif.movie().stop()
|
||||
self.gif_buffer.close()
|
||||
|
||||
image: Image.Image = Image.open(filepath)
|
||||
anim_image: Image.Image = image
|
||||
image_bytes_io: io.BytesIO = io.BytesIO()
|
||||
anim_image.save(
|
||||
image_bytes_io,
|
||||
"GIF",
|
||||
lossless=True,
|
||||
save_all=True,
|
||||
loop=0,
|
||||
disposal=2,
|
||||
)
|
||||
image_bytes_io.seek(0)
|
||||
ba: bytes = image_bytes_io.read()
|
||||
|
||||
self.gif_buffer.setData(ba)
|
||||
movie = QMovie(self.gif_buffer, QByteArray())
|
||||
self.preview_gif.setMovie(movie)
|
||||
movie.start()
|
||||
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(image.width, image.height),
|
||||
QSize(image.width, image.height),
|
||||
)
|
||||
)
|
||||
self.preview_img.hide()
|
||||
self.preview_vid.hide()
|
||||
self.preview_gif.show()
|
||||
|
||||
image = None
|
||||
if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RASTER_TYPES):
|
||||
image = Image.open(str(filepath))
|
||||
elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES):
|
||||
try:
|
||||
with rawpy.imread(str(filepath)) as raw:
|
||||
rgb = raw.postprocess()
|
||||
image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
|
||||
except (
|
||||
rawpy._rawpy.LibRawIOError,
|
||||
rawpy._rawpy.LibRawFileUnsupportedError,
|
||||
):
|
||||
pass
|
||||
elif MediaCategories.is_ext_in_category(ext, MediaCategories.AUDIO_TYPES):
|
||||
self.media_player.show()
|
||||
self.media_player.play(filepath)
|
||||
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()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
image = Image.fromarray(frame)
|
||||
if success:
|
||||
self.preview_img.hide()
|
||||
self.preview_vid.play(str(filepath), QSize(image.width, image.height))
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(image.width, image.height),
|
||||
QSize(image.width, image.height),
|
||||
)
|
||||
)
|
||||
self.preview_vid.show()
|
||||
|
||||
except (FileNotFoundError, cv2.error, UnidentifiedImageError, DecompressionBombError) as e:
|
||||
if self.preview_img.is_connected:
|
||||
self.preview_img.clicked.disconnect()
|
||||
self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth))
|
||||
self.preview_img.is_connected = True
|
||||
logger.error(f"Preview thumb error: {e} - {filepath}")
|
||||
|
||||
return {}
|
||||
133
tagstudio/src/qt/widgets/preview/recent_libraries.py
Normal file
133
tagstudio/src/qt/widgets/preview/recent_libraries.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import typing
|
||||
|
||||
import structlog
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.library.alchemy.library import Library
|
||||
from src.qt.translations import Translations
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class RecentLibraries(QWidget):
|
||||
"""The Recent Libraries Widget."""
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
|
||||
|
||||
# # keep list of rendered libraries to avoid needless re-rendering
|
||||
# self.render_libs: set = set()
|
||||
# self.library = library
|
||||
# self.driver = driver
|
||||
# layout = QVBoxLayout()
|
||||
|
||||
# settings = driver.settings
|
||||
# settings.beginGroup(SettingItems.LIBS_LIST)
|
||||
# lib_items: dict[str, tuple[str, str]] = {}
|
||||
# for item_tstamp in settings.allKeys():
|
||||
# val = str(settings.value(item_tstamp, type=str))
|
||||
# cut_val = val
|
||||
# if len(val) > 45:
|
||||
# cut_val = f"{val[0:10]} ... {val[-10:]}"
|
||||
# lib_items[item_tstamp] = (val, cut_val)
|
||||
|
||||
# settings.endGroup()
|
||||
|
||||
# new_keys = set(lib_items.keys())
|
||||
# if new_keys == self.render_libs:
|
||||
# # no need to re-render
|
||||
# return
|
||||
|
||||
# # sort lib_items by the key
|
||||
# libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True)
|
||||
|
||||
# self.render_libs = new_keys
|
||||
# self.setLayout(layout)
|
||||
|
||||
# self._fill_libs_widget(libs_sorted, layout)
|
||||
|
||||
# def _fill_libs_widget(self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout):
|
||||
# def clear_layout(layout_item: QVBoxLayout):
|
||||
# for i in reversed(range(layout_item.count())):
|
||||
# child = layout_item.itemAt(i)
|
||||
# if child.widget() is not None:
|
||||
# child.widget().deleteLater()
|
||||
# elif child.layout() is not None:
|
||||
# clear_layout(child.layout()) # type: ignore
|
||||
|
||||
# # remove any potential previous items
|
||||
# clear_layout(layout)
|
||||
|
||||
# label = QLabel()
|
||||
# Translations.translate_qobject(label, "generic.recent_libraries")
|
||||
# label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# row_layout = QHBoxLayout()
|
||||
# row_layout.addWidget(label)
|
||||
# layout.addLayout(row_layout)
|
||||
|
||||
# def set_button_style(
|
||||
# btn: QPushButtonWrapper | QPushButton, extras: list[str] | None = None
|
||||
# ):
|
||||
# base_style = [
|
||||
# f"background-color:{Theme.COLOR_BG.value};",
|
||||
# "border-radius:6px;",
|
||||
# "text-align: left;",
|
||||
# "padding-top: 3px;",
|
||||
# "padding-left: 6px;",
|
||||
# "padding-bottom: 4px;",
|
||||
# ]
|
||||
|
||||
# full_style_rows = base_style + (extras or [])
|
||||
|
||||
# btn.setStyleSheet(
|
||||
# "QPushButton{"
|
||||
# f"{''.join(full_style_rows)}"
|
||||
# "}"
|
||||
# f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER.value};}}"
|
||||
# f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED.value};}}"
|
||||
# f"QPushButton::disabled{{background-color:{Theme.COLOR_DISABLED_BG.value};}}"
|
||||
# )
|
||||
# btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# for item_key, (full_val, cut_val) in libraries:
|
||||
# button = QPushButton(text=cut_val)
|
||||
# button.setObjectName(f"path{item_key}")
|
||||
|
||||
# lib = Path(full_val)
|
||||
# if not lib.exists() or not (lib / TS_FOLDER_NAME).exists():
|
||||
# button.setDisabled(True)
|
||||
# Translations.translate_with_setter(button.setToolTip, "library.missing")
|
||||
|
||||
# def open_library_button_clicked(path):
|
||||
# return lambda: self.driver.open_library(Path(path))
|
||||
|
||||
# button.clicked.connect(open_library_button_clicked(full_val))
|
||||
# set_button_style(button, ["padding-left: 6px;", "text-align: left;"])
|
||||
# button_remove = QPushButton("—")
|
||||
# button_remove.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
# button_remove.setFixedWidth(24)
|
||||
# set_button_style(button_remove, ["font-weight:bold;", "text-align:center;"])
|
||||
|
||||
# def remove_recent_library_clicked(key: str):
|
||||
# return lambda: (
|
||||
# self.driver.remove_recent_library(key),
|
||||
# self.fill_libs_widget(self.libs_layout),
|
||||
# )
|
||||
|
||||
# button_remove.clicked.connect(remove_recent_library_clicked(item_key))
|
||||
|
||||
# row_layout = QHBoxLayout()
|
||||
# row_layout.addWidget(button)
|
||||
# row_layout.addWidget(button_remove)
|
||||
|
||||
# layout.addLayout(row_layout)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user