From f41e7d0a0f0b02626b673fb1f66bac6259f646f8 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 10 Jun 2024 16:45:26 -0700 Subject: [PATCH 1/6] 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 --- tagstudio/resources/{ => qt/images}/pause.svg | 0 tagstudio/resources/{ => qt/images}/play.svg | 0 .../images/volume.svg} | 0 .../images/volume_mute.svg} | 0 tagstudio/src/qt/widgets/video_player.py | 115 +++++++++--------- 5 files changed, 56 insertions(+), 59 deletions(-) rename tagstudio/resources/{ => qt/images}/pause.svg (100%) rename tagstudio/resources/{ => qt/images}/play.svg (100%) rename tagstudio/resources/{volume_unmuted.svg => qt/images/volume.svg} (100%) rename tagstudio/resources/{volume_muted.svg => qt/images/volume_mute.svg} (100%) diff --git a/tagstudio/resources/pause.svg b/tagstudio/resources/qt/images/pause.svg similarity index 100% rename from tagstudio/resources/pause.svg rename to tagstudio/resources/qt/images/pause.svg diff --git a/tagstudio/resources/play.svg b/tagstudio/resources/qt/images/play.svg similarity index 100% rename from tagstudio/resources/play.svg rename to tagstudio/resources/qt/images/play.svg diff --git a/tagstudio/resources/volume_unmuted.svg b/tagstudio/resources/qt/images/volume.svg similarity index 100% rename from tagstudio/resources/volume_unmuted.svg rename to tagstudio/resources/qt/images/volume.svg diff --git a/tagstudio/resources/volume_muted.svg b/tagstudio/resources/qt/images/volume_mute.svg similarity index 100% rename from tagstudio/resources/volume_muted.svg rename to tagstudio/resources/qt/images/volume_mute.svg diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 1ff98f880..8a6349582 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -1,8 +1,10 @@ +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import logging -import os -import typing -# os.environ["QT_MEDIA_BACKEND"] = "ffmpeg" +from pathlib import Path +import typing from PySide6.QtCore import ( Qt, @@ -18,7 +20,6 @@ from PySide6.QtMultimediaWidgets import QGraphicsVideoItem from PySide6.QtWidgets import QGraphicsView, QGraphicsScene from PySide6.QtGui import ( - QInputMethodEvent, QPen, QColor, QBrush, @@ -29,10 +30,7 @@ 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 @@ -51,6 +49,25 @@ class VideoPlayer(QGraphicsView): content_visible = False filepath = None + pause_icon: bytes = None + play_icon: bytes = None + volume_mute_icon: bytes = None + volume_icon: bytes = None + + # Load icon files as bytes + _parents = Path(__file__).parents[3] + with open(Path(_parents, "resources/qt/images/pause.svg"), "rb") as icon: + pause_icon = bytes(icon.read()) + + with open(Path(_parents, "resources/qt/images/play.svg"), "rb") as icon: + play_icon = bytes(icon.read()) + + with open(Path(_parents, "resources/qt/images/volume.svg"), "rb") as icon: + volume_icon = bytes(icon.read()) + + with open(Path(_parents, "resources/qt/images/volume_mute.svg"), "rb") as icon: + volume_mute_icon = bytes(icon.read()) + def __init__(self, driver: "QtDriver") -> None: # Set up the base class. super().__init__() @@ -61,6 +78,7 @@ def __init__(self, driver: "QtDriver") -> None: ) self.hover_fix_timer.timeout.connect(lambda: self.checkIfStillHovered()) self.hover_fix_timer.setSingleShot(True) + # Set up the video player. self.installEventFilter(self) self.setScene(QGraphicsScene(self)) @@ -82,6 +100,7 @@ def __init__(self, driver: "QtDriver") -> None: 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 +110,31 @@ def __init__(self, driver: "QtDriver") -> None: 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 +163,17 @@ def toggleAutoplay(self) -> None: 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 +181,14 @@ def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None: def updateControls(self) -> None: if self.player.audioOutput().isMuted(): - self.mute_button.load("./tagstudio/resources/volume_muted.svg") + self.mute_button.load(VideoPlayer.volume_mute_icon) else: - self.mute_button.load("./tagstudio/resources/volume_unmuted.svg") + self.mute_button.load(VideoPlayer.volume_icon) if self.player.isPlaying(): - self.play_pause.load("./tagstudio/resources/pause.svg") + self.play_pause.load(VideoPlayer.pause_icon) else: - self.play_pause.load("./tagstudio/resources/play.svg") + self.play_pause.load(VideoPlayer.play_icon) def wheelEvent(self, event: QWheelEvent) -> None: return @@ -229,8 +230,10 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: 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 +243,51 @@ def setTintTransparency(self, value) -> None: 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(VideoPlayer.pause_icon) + self.mute_button.load(VideoPlayer.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(VideoPlayer.play_icon) else: self.player.play() - self.play_pause.load("./tagstudio/resources/pause.svg") + self.play_pause.load(VideoPlayer.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(VideoPlayer.volume_icon) else: self.player.audioOutput().setMuted(True) - self.mute_button.load("./tagstudio/resources/volume_muted.svg") + self.mute_button.load(VideoPlayer.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 +296,6 @@ def play(self, filepath: str, resolution: QSize) -> None: self.player.play() else: self.checkMediaStatus(QMediaPlayer.MediaStatus.EndOfMedia) - # logging.info(f"Successfully stopped.") def stop(self) -> None: self.filepath = None @@ -310,10 +308,10 @@ def resizeVideo(self, new_size: QSize) -> None: 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 +344,6 @@ def keepControlsInPlace(self) -> None: 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 +355,6 @@ def resizeEvent(self, event: QResizeEvent) -> None: ) ) return - # return super().resizeEvent(event)\ class VideoPreview(QGraphicsVideoItem): @@ -367,7 +363,8 @@ def boundingRect(self): 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) From cd3709a4fc3f23a9fcefb78d8bb3e8179c8a7968 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 12 Jun 2024 16:35:01 -0700 Subject: [PATCH 2/6] Add basic ResourceManager, use in video_player.py --- tagstudio.spec | 77 +++++------------------- tagstudio/src/qt/resource_manager.py | 66 ++++++++++++++++++++ tagstudio/src/qt/resources.json | 18 ++++++ tagstudio/src/qt/widgets/video_player.py | 52 ++++++---------- 4 files changed, 116 insertions(+), 97 deletions(-) create mode 100644 tagstudio/src/qt/resource_manager.py create mode 100644 tagstudio/src/qt/resources.json diff --git a/tagstudio.spec b/tagstudio.spec index d4b9d9fea..43dc4767a 100644 --- a/tagstudio.spec +++ b/tagstudio.spec @@ -1,86 +1,39 @@ # -*- mode: python ; coding: utf-8 -*- -# vi: ft=python - - -from argparse import ArgumentParser -import sys -from PyInstaller.building.api import COLLECT, EXE, PYZ -from PyInstaller.building.build_main import Analysis -from PyInstaller.building.osx import BUNDLE - - -parser = ArgumentParser() -parser.add_argument('--portable', action='store_true') -options = parser.parse_args() - - -name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio' -icon = None -if sys.platform == 'win32': - icon = 'tagstudio/resources/icon.ico' -elif sys.platform == 'darwin': - icon = 'tagstudio/resources/icon.icns' a = Analysis( - ['tagstudio/tag_studio.py'], - pathex=[], + ['tagstudio\\tag_studio.py'], + pathex=['tagstudio'], binaries=[], - datas=[('tagstudio/resources', 'resources')], + datas=[('tagstudio/resources', './resources'), ('tagstudio/src', './src')], hiddenimports=[], hookspath=[], hooksconfig={}, - excludes=[], runtime_hooks=[], + excludes=[], noarchive=False, optimize=0, ) - pyz = PYZ(a.pure) -include = [a.scripts] -if options.portable: - include += (a.binaries, a.datas) exe = EXE( pyz, - *include, + a.scripts, + a.binaries, + a.datas, [], + name='TagStudio', + debug=False, bootloader_ignore_signals=False, - console=False, - hide_console='hide-early', + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, disable_windowed_traceback=False, - debug=False, - name=name, - exclude_binaries=not options.portable, - icon=icon, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None -) - -coll = None if options.portable else COLLECT( - exe, - a.binaries, - a.datas, - name=name, - strip=False, - upx=True, - upx_exclude=[], -) - -app = BUNDLE( - exe if coll is None else coll, - name='TagStudio.app', - icon=icon, - bundle_identifier='com.github.tagstudiodev', - version='0.0.0', - info_plist={ - 'NSAppleScriptEnabled': False, - 'NSPrincipalClass': 'NSApplication', - } + icon=['tagstudio\\resources\\icon.ico'], ) diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py new file mode 100644 index 000000000..d442ff08a --- /dev/null +++ b/tagstudio/src/qt/resource_manager.py @@ -0,0 +1,66 @@ +# 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[str, tuple[str, str]] = {} # dict[, tuple[,]] + _cache: dict[str, Any] = {} + + # Initialize _map + with open( + Path(__file__).parent / "resources.json", mode="r", encoding="utf-8" + ) as f: + json_map: dict = ujson.load(f) + for item in json_map.items(): + _map[item[0]] = (item[1]["path"], item[1]["mode"]) + + logging.info(f"[ResourceManager] Resources Loaded: {_map}") + + def __init__(self) -> None: + pass + + 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, None) + if cached_res: + return cached_res + else: + path, mode = ResourceManager._map.get(id, None) + if mode in ["r", "rb"]: + with open((Path(__file__).parents[2] / "resources" / path), mode) as f: + data = f.read() + + if mode == ["rb"]: + data = bytes(data) + + ResourceManager._cache[id] = data + return data + elif 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") diff --git a/tagstudio/src/qt/resources.json b/tagstudio/src/qt/resources.json new file mode 100644 index 000000000..1f8663d37 --- /dev/null +++ b/tagstudio/src/qt/resources.json @@ -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" + } +} diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 8a6349582..1b6dcf006 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -33,51 +33,33 @@ from src.qt.helpers.file_opener import FileOpenerHelper from PIL import Image, ImageDraw from src.core.enums import SettingItems +from src.qt.resource_manager import ResourceManager if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver class VideoPlayer(QGraphicsView): - """A simple video player for the TagStudio application.""" + """A basic video player.""" - resolution = QSize(1280, 720) - hover_fix_timer = QTimer() + rm: ResourceManager = ResourceManager() video_preview = None play_pause = None mute_button = None - content_visible = False - filepath = None - - pause_icon: bytes = None - play_icon: bytes = None - volume_mute_icon: bytes = None - volume_icon: bytes = None - - # Load icon files as bytes - _parents = Path(__file__).parents[3] - with open(Path(_parents, "resources/qt/images/pause.svg"), "rb") as icon: - pause_icon = bytes(icon.read()) - - with open(Path(_parents, "resources/qt/images/play.svg"), "rb") as icon: - play_icon = bytes(icon.read()) - - with open(Path(_parents, "resources/qt/images/volume.svg"), "rb") as icon: - volume_icon = bytes(icon.read()) - - with open(Path(_parents, "resources/qt/images/volume_mute.svg"), "rb") as icon: - volume_mute_icon = bytes(icon.read()) 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) @@ -181,14 +163,14 @@ def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None: def updateControls(self) -> None: if self.player.audioOutput().isMuted(): - self.mute_button.load(VideoPlayer.volume_mute_icon) + self.mute_button.load(VideoPlayer.rm.volume_mute_icon) else: - self.mute_button.load(VideoPlayer.volume_icon) + self.mute_button.load(VideoPlayer.rm.volume_icon) if self.player.isPlaying(): - self.play_pause.load(VideoPlayer.pause_icon) + self.play_pause.load(VideoPlayer.rm.pause_icon) else: - self.play_pause.load(VideoPlayer.play_icon) + self.play_pause.load(VideoPlayer.rm.play_icon) def wheelEvent(self, event: QWheelEvent) -> None: return @@ -266,24 +248,24 @@ def releaseMouse(self) -> None: def resetControlsToDefault(self) -> None: # Resets the video controls to their default state. - self.play_pause.load(VideoPlayer.pause_icon) - self.mute_button.load(VideoPlayer.volume_mute_icon) + self.play_pause.load(VideoPlayer.rm.pause_icon) + self.mute_button.load(VideoPlayer.rm.volume_mute_icon) def pauseToggle(self) -> None: if self.player.isPlaying(): self.player.pause() - self.play_pause.load(VideoPlayer.play_icon) + self.play_pause.load(VideoPlayer.rm.play_icon) else: self.player.play() - self.play_pause.load(VideoPlayer.pause_icon) + self.play_pause.load(VideoPlayer.rm.pause_icon) def muteToggle(self) -> None: if self.player.audioOutput().isMuted(): self.player.audioOutput().setMuted(False) - self.mute_button.load(VideoPlayer.volume_icon) + self.mute_button.load(VideoPlayer.rm.volume_icon) else: self.player.audioOutput().setMuted(True) - self.mute_button.load(VideoPlayer.volume_mute_icon) + self.mute_button.load(VideoPlayer.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, From a99476a9ebdbb68cfcf00fbd428cb16685d05aaa Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 12 Jun 2024 16:45:39 -0700 Subject: [PATCH 3/6] Revert tagstudio.spec changes --- tagstudio.spec | 77 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/tagstudio.spec b/tagstudio.spec index 43dc4767a..d4b9d9fea 100644 --- a/tagstudio.spec +++ b/tagstudio.spec @@ -1,39 +1,86 @@ # -*- mode: python ; coding: utf-8 -*- +# vi: ft=python + + +from argparse import ArgumentParser +import sys +from PyInstaller.building.api import COLLECT, EXE, PYZ +from PyInstaller.building.build_main import Analysis +from PyInstaller.building.osx import BUNDLE + + +parser = ArgumentParser() +parser.add_argument('--portable', action='store_true') +options = parser.parse_args() + + +name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio' +icon = None +if sys.platform == 'win32': + icon = 'tagstudio/resources/icon.ico' +elif sys.platform == 'darwin': + icon = 'tagstudio/resources/icon.icns' a = Analysis( - ['tagstudio\\tag_studio.py'], - pathex=['tagstudio'], + ['tagstudio/tag_studio.py'], + pathex=[], binaries=[], - datas=[('tagstudio/resources', './resources'), ('tagstudio/src', './src')], + datas=[('tagstudio/resources', 'resources')], hiddenimports=[], hookspath=[], hooksconfig={}, - runtime_hooks=[], excludes=[], + runtime_hooks=[], noarchive=False, optimize=0, ) + pyz = PYZ(a.pure) +include = [a.scripts] +if options.portable: + include += (a.binaries, a.datas) exe = EXE( pyz, - a.scripts, - a.binaries, - a.datas, + *include, [], - name='TagStudio', - debug=False, bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, + console=False, + hide_console='hide-early', disable_windowed_traceback=False, + debug=False, + name=name, + exclude_binaries=not options.portable, + icon=icon, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, - icon=['tagstudio\\resources\\icon.ico'], + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None +) + +coll = None if options.portable else COLLECT( + exe, + a.binaries, + a.datas, + name=name, + strip=False, + upx=True, + upx_exclude=[], +) + +app = BUNDLE( + exe if coll is None else coll, + name='TagStudio.app', + icon=icon, + bundle_identifier='com.github.tagstudiodev', + version='0.0.0', + info_plist={ + 'NSAppleScriptEnabled': False, + 'NSPrincipalClass': 'NSApplication', + } ) From cadbceb1f5e8627e9ae2902aa2b956c86eef4af8 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 12 Jun 2024 22:57:47 -0700 Subject: [PATCH 4/6] Change tuple usage to dicts --- tagstudio/src/qt/resource_manager.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index d442ff08a..f0d9106e2 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -14,16 +14,14 @@ class ResourceManager: """A resource manager for retrieving resources.""" - _map: dict[str, tuple[str, str]] = {} # dict[, tuple[,]] + _map: dict = {} _cache: dict[str, Any] = {} # Initialize _map with open( Path(__file__).parent / "resources.json", mode="r", encoding="utf-8" ) as f: - json_map: dict = ujson.load(f) - for item in json_map.items(): - _map[item[0]] = (item[1]["path"], item[1]["mode"]) + _map = ujson.load(f) logging.info(f"[ResourceManager] Resources Loaded: {_map}") @@ -41,21 +39,22 @@ def get(self, id: str) -> Any: Returns: Any: The resource if found, else None. """ - cached_res = ResourceManager._cache.get(id, None) + cached_res = ResourceManager._cache.get(id) if cached_res: return cached_res else: - path, mode = ResourceManager._map.get(id, None) - if mode in ["r", "rb"]: - with open((Path(__file__).parents[2] / "resources" / path), mode) as f: + 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 mode == ["rb"]: + if res.get("mode") == ["rb"]: data = bytes(data) - ResourceManager._cache[id] = data return data - elif mode in ["qt"]: + elif res.get("mode") in ["qt"]: # TODO: Qt resource loading logic pass From 50375306e97f127c9dd55acdec043be70b307c9b Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 12 Jun 2024 23:10:24 -0700 Subject: [PATCH 5/6] Move ResourceManager initialization steps --- tagstudio/src/qt/resource_manager.py | 20 +++++++++++--------- tagstudio/src/qt/ts_qt.py | 2 ++ tagstudio/src/qt/widgets/video_player.py | 22 ++++++++++------------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index f0d9106e2..7cdb24b75 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -16,17 +16,19 @@ class ResourceManager: _map: dict = {} _cache: dict[str, Any] = {} - - # Initialize _map - with open( - Path(__file__).parent / "resources.json", mode="r", encoding="utf-8" - ) as f: - _map = ujson.load(f) - - logging.info(f"[ResourceManager] Resources Loaded: {_map}") + _initialized: bool = False def __init__(self) -> None: - pass + # 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. diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6da290005..e53f82123 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -70,6 +70,7 @@ 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 @@ def __init__(self, core: TagStudioCore, args): 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] = [] diff --git a/tagstudio/src/qt/widgets/video_player.py b/tagstudio/src/qt/widgets/video_player.py index 1b6dcf006..6bb860993 100644 --- a/tagstudio/src/qt/widgets/video_player.py +++ b/tagstudio/src/qt/widgets/video_player.py @@ -33,7 +33,6 @@ from src.qt.helpers.file_opener import FileOpenerHelper from PIL import Image, ImageDraw from src.core.enums import SettingItems -from src.qt.resource_manager import ResourceManager if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -42,7 +41,6 @@ class VideoPlayer(QGraphicsView): """A basic video player.""" - rm: ResourceManager = ResourceManager() video_preview = None play_pause = None mute_button = None @@ -163,14 +161,14 @@ def checkMediaStatus(self, media_status: QMediaPlayer.MediaStatus) -> None: def updateControls(self) -> None: if self.player.audioOutput().isMuted(): - self.mute_button.load(VideoPlayer.rm.volume_mute_icon) + self.mute_button.load(self.driver.rm.volume_mute_icon) else: - self.mute_button.load(VideoPlayer.rm.volume_icon) + self.mute_button.load(self.driver.rm.volume_icon) if self.player.isPlaying(): - self.play_pause.load(VideoPlayer.rm.pause_icon) + self.play_pause.load(self.driver.rm.pause_icon) else: - self.play_pause.load(VideoPlayer.rm.play_icon) + self.play_pause.load(self.driver.rm.play_icon) def wheelEvent(self, event: QWheelEvent) -> None: return @@ -248,24 +246,24 @@ def releaseMouse(self) -> None: def resetControlsToDefault(self) -> None: # Resets the video controls to their default state. - self.play_pause.load(VideoPlayer.rm.pause_icon) - self.mute_button.load(VideoPlayer.rm.volume_mute_icon) + 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(VideoPlayer.rm.play_icon) + self.play_pause.load(self.driver.rm.play_icon) else: self.player.play() - self.play_pause.load(VideoPlayer.rm.pause_icon) + 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(VideoPlayer.rm.volume_icon) + self.mute_button.load(self.driver.rm.volume_icon) else: self.player.audioOutput().setMuted(True) - self.mute_button.load(VideoPlayer.rm.volume_mute_icon) + 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, From 369d212e4cc1b1ae66e1b9081fb0cccb83cfb350 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 12 Jun 2024 23:13:53 -0700 Subject: [PATCH 6/6] Fix errant list notation --- tagstudio/src/qt/resource_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index 7cdb24b75..0db8bb194 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -52,7 +52,7 @@ def get(self, id: str) -> Any: res.get("mode"), ) as f: data = f.read() - if res.get("mode") == ["rb"]: + if res.get("mode") == "rb": data = bytes(data) ResourceManager._cache[id] = data return data