* Refactor video_player.py - Move icons files to qt/images folder, some being renamed - Reduce icon loading to single initial import - Tweak icon dimensions and animation timings - Remove unnecessary commented code - Remove unused/duplicate imports - Add license info to file * Add basic ResourceManager, use in video_player.py * Revert tagstudio.spec changes * Change tuple usage to dicts * Move ResourceManager initialization steps * Fix errant list notation
Before Width: | Height: | Size: 172 B After Width: | Height: | Size: 172 B |
Before Width: | Height: | Size: 151 B After Width: | Height: | Size: 151 B |
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 445 B |
67
tagstudio/src/qt/resource_manager.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import ujson
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
|
||||
class ResourceManager:
|
||||
"""A resource manager for retrieving resources."""
|
||||
|
||||
_map: dict = {}
|
||||
_cache: dict[str, Any] = {}
|
||||
_initialized: bool = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Load JSON resource map
|
||||
if not ResourceManager._initialized:
|
||||
with open(
|
||||
Path(__file__).parent / "resources.json", mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
ResourceManager._map = ujson.load(f)
|
||||
logging.info(
|
||||
f"[ResourceManager] {len(ResourceManager._map.items())} resources registered"
|
||||
)
|
||||
ResourceManager._initialized = True
|
||||
|
||||
def get(self, id: str) -> Any:
|
||||
"""Get a resource from the ResourceManager.
|
||||
This can include resources inside and outside of QResources, and will return
|
||||
theme-respecting variations of resources if available.
|
||||
|
||||
Args:
|
||||
id (str): The name of the resource.
|
||||
|
||||
Returns:
|
||||
Any: The resource if found, else None.
|
||||
"""
|
||||
cached_res = ResourceManager._cache.get(id)
|
||||
if cached_res:
|
||||
return cached_res
|
||||
else:
|
||||
res: dict = ResourceManager._map.get(id)
|
||||
if res.get("mode") in ["r", "rb"]:
|
||||
with open(
|
||||
(Path(__file__).parents[2] / "resources" / res.get("path")),
|
||||
res.get("mode"),
|
||||
) as f:
|
||||
data = f.read()
|
||||
if res.get("mode") == "rb":
|
||||
data = bytes(data)
|
||||
ResourceManager._cache[id] = data
|
||||
return data
|
||||
elif res.get("mode") in ["qt"]:
|
||||
# TODO: Qt resource loading logic
|
||||
pass
|
||||
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
attr = self.get(__name)
|
||||
if attr:
|
||||
return attr
|
||||
raise AttributeError(f"Attribute {id} not found")
|
18
tagstudio/src/qt/resources.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"play_icon": {
|
||||
"path": "qt/images/play.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"pause_icon": {
|
||||
"path": "qt/images/pause.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_icon": {
|
||||
"path": "qt/images/volume.svg",
|
||||
"mode": "rb"
|
||||
},
|
||||
"volume_mute_icon": {
|
||||
"path": "qt/images/volume_mute.svg",
|
||||
"mode": "rb"
|
||||
}
|
||||
}
|
|
@ -70,6 +70,7 @@ from src.qt.flowlayout import FlowLayout
|
|||
from src.qt.main_window import Ui_MainWindow
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.resource_manager import ResourceManager
|
||||
from src.qt.widgets.collage_icon import CollageIconRenderer
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
@ -164,6 +165,7 @@ class QtDriver(QObject):
|
|||
super().__init__()
|
||||
self.core: TagStudioCore = core
|
||||
self.lib = self.core.lib
|
||||
self.rm: ResourceManager = ResourceManager()
|
||||
self.args = args
|
||||
self.frame_dict: dict = {}
|
||||
self.nav_frames: list[NavigationState] = []
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
import os
|
||||
import typing
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
# os.environ["QT_MEDIA_BACKEND"] = "ffmpeg"
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
import typing
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt,
|
||||
|
@ -18,7 +20,6 @@ from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
|
|||
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene
|
||||
from PySide6.QtGui import (
|
||||
QInputMethodEvent,
|
||||
QPen,
|
||||
QColor,
|
||||
QBrush,
|
||||
|
@ -29,10 +30,7 @@ from PySide6.QtGui import (
|
|||
QBitmap,
|
||||
)
|
||||
from PySide6.QtSvgWidgets import QSvgWidget
|
||||
from PIL import Image
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper
|
||||
|
||||
from src.core.constants import VIDEO_TYPES, AUDIO_TYPES
|
||||
from PIL import Image, ImageDraw
|
||||
from src.core.enums import SettingItems
|
||||
|
||||
|
@ -41,26 +39,26 @@ if typing.TYPE_CHECKING:
|
|||
|
||||
|
||||
class VideoPlayer(QGraphicsView):
|
||||
"""A simple video player for the TagStudio application."""
|
||||
"""A basic video player."""
|
||||
|
||||
resolution = QSize(1280, 720)
|
||||
hover_fix_timer = QTimer()
|
||||
video_preview = None
|
||||
play_pause = None
|
||||
mute_button = None
|
||||
content_visible = False
|
||||
filepath = None
|
||||
|
||||
def __init__(self, driver: "QtDriver") -> None:
|
||||
# Set up the base class.
|
||||
super().__init__()
|
||||
self.driver = driver
|
||||
self.resolution = QSize(1280, 720)
|
||||
self.animation = QVariantAnimation(self)
|
||||
self.animation.valueChanged.connect(
|
||||
lambda value: self.setTintTransparency(value)
|
||||
)
|
||||
self.hover_fix_timer = QTimer()
|
||||
self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered())
|
||||
self.hover_fix_timer.setSingleShot(True)
|
||||
self.content_visible = False
|
||||
self.filepath = None
|
||||
|
||||
# Set up the video player.
|
||||
self.installEventFilter(self)
|
||||
self.setScene(QGraphicsScene(self))
|
||||
|
@ -82,6 +80,7 @@ class VideoPlayer(QGraphicsView):
|
|||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.scene().addItem(self.video_preview)
|
||||
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
||||
|
||||
# Set up the video tint.
|
||||
self.video_tint = self.scene().addRect(
|
||||
0,
|
||||
|
@ -91,44 +90,31 @@ class VideoPlayer(QGraphicsView):
|
|||
QPen(QColor(0, 0, 0, 0)),
|
||||
QBrush(QColor(0, 0, 0, 0)),
|
||||
)
|
||||
# self.video_tint.setParentItem(self.video_preview)
|
||||
# self.album_art = QGraphicsPixmapItem(self.video_preview)
|
||||
# self.scene().addItem(self.album_art)
|
||||
# self.album_art.setPixmap(
|
||||
# QPixmap("./tagstudio/resources/qt/images/thumb_file_default_512.png")
|
||||
# )
|
||||
# self.album_art.setOpacity(0.0)
|
||||
|
||||
# Set up the buttons.
|
||||
self.play_pause = QSvgWidget("./tagstudio/resources/pause.svg")
|
||||
self.play_pause = QSvgWidget()
|
||||
self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.play_pause.setMouseTracking(True)
|
||||
self.play_pause.installEventFilter(self)
|
||||
self.scene().addWidget(self.play_pause)
|
||||
self.play_pause.resize(100, 100)
|
||||
self.play_pause.resize(72, 72)
|
||||
self.play_pause.move(
|
||||
int(self.width() / 2 - self.play_pause.size().width() / 2),
|
||||
int(self.height() / 2 - self.play_pause.size().height() / 2),
|
||||
)
|
||||
self.play_pause.hide()
|
||||
|
||||
self.mute_button = QSvgWidget("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button = QSvgWidget()
|
||||
self.mute_button.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.mute_button.setMouseTracking(True)
|
||||
self.mute_button.installEventFilter(self)
|
||||
self.scene().addWidget(self.mute_button)
|
||||
self.mute_button.resize(40, 40)
|
||||
self.mute_button.resize(32, 32)
|
||||
self.mute_button.move(
|
||||
int(self.width() - self.mute_button.size().width() / 2),
|
||||
int(self.height() - self.mute_button.size().height() / 2),
|
||||
)
|
||||
self.mute_button.hide()
|
||||
# self.fullscreen_button = QSvgWidget('./tagstudio/resources/pause.svg', self)
|
||||
# self.fullscreen_button.setMouseTracking(True)
|
||||
# self.fullscreen_button.installEventFilter(self)
|
||||
# self.scene().addWidget(self.fullscreen_button)
|
||||
# self.fullscreen_button.resize(40, 40)
|
||||
# self.fullscreen_button.move(self.fullscreen_button.size().width()/2, self.height() - self.fullscreen_button.size().height()/2)
|
||||
# self.fullscreen_button.hide()
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.opener = FileOpenerHelper(filepath=self.filepath)
|
||||
|
@ -157,22 +143,17 @@ class VideoPlayer(QGraphicsView):
|
|||
self.driver.settings.sync()
|
||||
|
||||
def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None:
|
||||
# logging.info(media_status)
|
||||
if media_status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
# Switches current video to with video at filepath. Reason for this is because Pyside6 is dumb and can't handle setting a new source and freezes.
|
||||
# Switches current video to with video at filepath.
|
||||
# Reason for this is because Pyside6 can't handle setting a new source and freezes.
|
||||
# Even if I stop the player before switching, it breaks.
|
||||
# On the plus side, this adds infinite looping for the video preview.
|
||||
self.player.stop()
|
||||
self.player.setSource(QUrl().fromLocalFile(self.filepath))
|
||||
# logging.info(f'Set source to {self.filepath}.')
|
||||
# self.video_preview.setSize(self.resolution)
|
||||
self.player.setPosition(0)
|
||||
# logging.info(f'Set muted to true.')
|
||||
if self.autoplay.isChecked():
|
||||
# logging.info(self.driver.settings.value("autoplay_videos", True, bool))
|
||||
self.player.play()
|
||||
else:
|
||||
# logging.info("Paused")
|
||||
self.player.pause()
|
||||
self.opener.set_filepath(self.filepath)
|
||||
self.keepControlsInPlace()
|
||||
|
@ -180,14 +161,14 @@ class VideoPlayer(QGraphicsView):
|
|||
|
||||
def updateControls(self) -> None:
|
||||
if self.player.audioOutput().isMuted():
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
else:
|
||||
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_icon)
|
||||
|
||||
if self.player.isPlaying():
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
else:
|
||||
self.play_pause.load("./tagstudio/resources/play.svg")
|
||||
self.play_pause.load(self.driver.rm.play_icon)
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent) -> None:
|
||||
return
|
||||
|
@ -229,8 +210,10 @@ class VideoPlayer(QGraphicsView):
|
|||
return super().eventFilter(obj, event)
|
||||
|
||||
def checkIfStillHovered(self) -> None:
|
||||
# Yet again, Pyside6 is dumb. I don't know why, but the HoverLeave event is not triggered sometimes and does not hide the controls.
|
||||
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse is still in the video preview.
|
||||
# I don't know why, but the HoverLeave event is not triggered sometimes
|
||||
# and does not hide the controls.
|
||||
# So, this is a workaround. This is called by a QTimer every 10ms to check if the mouse
|
||||
# is still in the video preview.
|
||||
if not self.video_preview.isUnderMouse():
|
||||
self.releaseMouse()
|
||||
else:
|
||||
|
@ -240,55 +223,51 @@ class VideoPlayer(QGraphicsView):
|
|||
self.video_tint.setBrush(QBrush(QColor(0, 0, 0, value)))
|
||||
|
||||
def underMouse(self) -> bool:
|
||||
# logging.info("under mouse")
|
||||
self.animation.setStartValue(self.video_tint.brush().color().alpha())
|
||||
self.animation.setEndValue(100)
|
||||
self.animation.setDuration(500)
|
||||
self.animation.setDuration(250)
|
||||
self.animation.start()
|
||||
self.play_pause.show()
|
||||
self.mute_button.show()
|
||||
# self.fullscreen_button.show()
|
||||
self.keepControlsInPlace()
|
||||
self.updateControls()
|
||||
# rcontent = self.contentsRect()
|
||||
# self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
|
||||
|
||||
return super().underMouse()
|
||||
|
||||
def releaseMouse(self) -> None:
|
||||
# logging.info("release mouse")
|
||||
self.animation.setStartValue(self.video_tint.brush().color().alpha())
|
||||
self.animation.setEndValue(0)
|
||||
self.animation.setDuration(500)
|
||||
self.animation.start()
|
||||
self.play_pause.hide()
|
||||
self.mute_button.hide()
|
||||
# self.fullscreen_button.hide()
|
||||
|
||||
return super().releaseMouse()
|
||||
|
||||
def resetControlsToDefault(self) -> None:
|
||||
# Resets the video controls to their default state.
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
|
||||
def pauseToggle(self) -> None:
|
||||
if self.player.isPlaying():
|
||||
self.player.pause()
|
||||
self.play_pause.load("./tagstudio/resources/play.svg")
|
||||
self.play_pause.load(self.driver.rm.play_icon)
|
||||
else:
|
||||
self.player.play()
|
||||
self.play_pause.load("./tagstudio/resources/pause.svg")
|
||||
self.play_pause.load(self.driver.rm.pause_icon)
|
||||
|
||||
def muteToggle(self) -> None:
|
||||
if self.player.audioOutput().isMuted():
|
||||
self.player.audioOutput().setMuted(False)
|
||||
self.mute_button.load("./tagstudio/resources/volume_unmuted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_icon)
|
||||
else:
|
||||
self.player.audioOutput().setMuted(True)
|
||||
self.mute_button.load("./tagstudio/resources/volume_muted.svg")
|
||||
self.mute_button.load(self.driver.rm.volume_mute_icon)
|
||||
|
||||
def play(self, filepath: str, resolution: QSize) -> None:
|
||||
# Sets the filepath and sends the current player position to the very end, so that the new video can be played.
|
||||
# self.player.audioOutput().setMuted(True)
|
||||
# Sets the filepath and sends the current player position to the very end,
|
||||
# so that the new video can be played.
|
||||
logging.info(f"Playing {filepath}")
|
||||
self.resolution = resolution
|
||||
self.filepath = filepath
|
||||
|
@ -297,7 +276,6 @@ class VideoPlayer(QGraphicsView):
|
|||
self.player.play()
|
||||
else:
|
||||
self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia)
|
||||
# logging.info(f"Successfully stopped.")
|
||||
|
||||
def stop(self) -> None:
|
||||
self.filepath = None
|
||||
|
@ -310,10 +288,10 @@ class VideoPlayer(QGraphicsView):
|
|||
0, 0, self.video_preview.size().width(), self.video_preview.size().height()
|
||||
)
|
||||
|
||||
rcontent = self.contentsRect()
|
||||
contents = self.contentsRect()
|
||||
self.centerOn(self.video_preview)
|
||||
self.roundCorners()
|
||||
self.setSceneRect(0, 0, rcontent.width(), rcontent.height())
|
||||
self.setSceneRect(0, 0, contents.width(), contents.height())
|
||||
self.keepControlsInPlace()
|
||||
|
||||
def roundCorners(self) -> None:
|
||||
|
@ -346,7 +324,6 @@ class VideoPlayer(QGraphicsView):
|
|||
int(self.width() - self.mute_button.size().width() - 10),
|
||||
int(self.height() - self.mute_button.size().height() - 10),
|
||||
)
|
||||
# self.fullscreen_button.move(-self.fullscreen_button.size().width()-10, self.height() - self.fullscreen_button.size().height()-10)
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
# Keeps the video preview in the center of the screen.
|
||||
|
@ -358,7 +335,6 @@ class VideoPlayer(QGraphicsView):
|
|||
)
|
||||
)
|
||||
return
|
||||
# return super().resizeEvent(event)\
|
||||
|
||||
|
||||
class VideoPreview(QGraphicsVideoItem):
|
||||
|
@ -367,7 +343,8 @@ class VideoPreview(QGraphicsVideoItem):
|
|||
|
||||
def paint(self, painter, option, widget):
|
||||
# painter.brush().setColor(QColor(0, 0, 0, 255))
|
||||
# You can set any shape you want here. RoundedRect is the standard rectangle with rounded corners
|
||||
# You can set any shape you want here.
|
||||
# RoundedRect is the standard rectangle with rounded corners.
|
||||
# With 2nd and 3rd parameter you can tweak the curve until you get what you expect
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
|
|