feat(ui): add configurable splash screens (#703)

* refactor: move splash images to resource_manager

* feat(ui): add new splash screen widget

* style: restore old resource manager log style

* fix: scale font size for Segoe UI
This commit is contained in:
Travis Abendshien
2025-01-22 16:02:58 -08:00
committed by GitHub
parent 857f40f2e3
commit a02c43c115
9 changed files with 204 additions and 25477 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

View File

@@ -7,7 +7,11 @@ from typing import Any
import structlog
import ujson
from PIL import Image
from PIL import (
Image,
ImageQt,
)
from PySide6.QtGui import QPixmap
logger = structlog.get_logger(__name__)
@@ -25,7 +29,10 @@ class ResourceManager:
if not ResourceManager._initialized:
with open(Path(__file__).parent / "resources.json", encoding="utf-8") as f:
ResourceManager._map = ujson.load(f)
logger.info("resources registered", count=len(ResourceManager._map.items()))
logger.info(
"[ResourceManager] Resources Registered:",
count=len(ResourceManager._map.items()),
)
ResourceManager._initialized = True
@staticmethod
@@ -76,12 +83,15 @@ class ResourceManager:
elif res and res.get("mode") == "pil":
data = Image.open(ResourceManager._res_folder / "resources" / res.get("path"))
return data
elif res.get("mode") in ["qt"]:
# TODO: Qt resource loading logic
pass
elif res.get("mode") in ["qpixmap"]:
data = Image.open(ResourceManager._res_folder / "resources" / res.get("path"))
qim = ImageQt.ImageQt(data)
pixmap = QPixmap.fromImage(qim)
ResourceManager._cache[id] = pixmap
return pixmap
except FileNotFoundError:
path: Path = ResourceManager._res_folder / "resources" / res.get("path")
logger.error("[ResourceManager][ERROR]: Could not find resource: ", path)
logger.error("[ResourceManager][ERROR]: Could not find resource: ", path=path)
return None
def __getattr__(self, __name: str) -> Any:

View File

@@ -1,4 +1,12 @@
{
"splash_classic": {
"path": "qt/images/splash/classic.png",
"mode": "qpixmap"
},
"splash_goo_gears": {
"path": "qt/images/splash/goo_gears.png",
"mode": "qpixmap"
},
"logo": {
"path": "icon.png",
"mode": "pil"

View File

@@ -8,6 +8,5 @@
<!-- <file alias = "images/edit_icon_128.png">../../resources/qt/images/edit_icon_128.png</file> -->
<!-- <file alias = "images/trash_icon_128.png">../../resources/qt/images/trash_icon_128.png</file> -->
<!-- <file alias = "images/clipboard_icon_128.png">../../resources/qt/images/clipboard_icon_128.png</file> -->
<file alias = "images/splash.png">../../resources/qt/images/splash.png</file>
</qresource>
</RCC>

File diff suppressed because it is too large Load Diff

170
tagstudio/src/qt/splash.py Normal file
View File

@@ -0,0 +1,170 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import math
import structlog
from PySide6.QtCore import QRect, Qt
from PySide6.QtGui import (
QColor,
QFont,
QPainter,
QPen,
QPixmap,
)
from PySide6.QtWidgets import (
QSplashScreen,
QWidget,
)
from src.core.constants import (
VERSION,
VERSION_BRANCH,
)
from src.qt.resource_manager import ResourceManager
logger = structlog.get_logger(__name__)
class Splash:
"""The custom splash screen widget for TagStudio."""
COPYRIGHT_YEARS: str = "2021-2025"
COPYRIGHT_STR: str = f"© {COPYRIGHT_YEARS} Travis Abendshien (CyanVoxel)"
VERSION_STR: str = (
f"Version {VERSION} {(" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else ""}"
)
SPLASH_CLASSIC: str = "classic"
SPLASH_GOO_GEARS: str = "goo_gears"
SPLASH_95: str = "95"
DEFAULT_SPLASH: str = SPLASH_GOO_GEARS
def __init__(
self,
resource_manager: ResourceManager,
screen_width: int,
splash_name: str,
device_ratio: float = 1,
):
self.rm = resource_manager
self.screen_width = screen_width
self.ratio: float = device_ratio
self.splash_screen: QSplashScreen | None = None
self.splash_name: str = splash_name if splash_name else Splash.DEFAULT_SPLASH
def get_pixmap(self) -> QPixmap:
"""Get the pixmap used for the splash screen."""
pixmap: QPixmap | None = self.rm.get(f"splash_{self.splash_name}")
if not pixmap:
logger.error("[Splash] Splash screen not found:", splash_name=self.splash_name)
pixmap = QPixmap(960, 540)
pixmap.fill(QColor("black"))
painter = QPainter(pixmap)
point_size_scale: float = 1.0
match painter.font().family():
case "Segoe UI":
point_size_scale = 0.75
# TODO: Store any differing data elsewhere and load dynamically instead of hardcoding.
match self.splash_name:
case Splash.SPLASH_CLASSIC:
# Copyright
font = painter.font()
font.setPointSize(math.floor(22 * point_size_scale))
painter.setFont(font)
pen = QPen(QColor("#9782ff"))
painter.setPen(pen)
painter.drawText(
QRect(0, -50, 960, 540),
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
Splash.COPYRIGHT_STR,
)
# Version
pen = QPen(QColor("#809782ff"))
painter.setPen(pen)
painter.drawText(
QRect(0, -25, 960, 540),
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
Splash.VERSION_STR,
)
case Splash.SPLASH_GOO_GEARS:
# Copyright
font = painter.font()
font.setPointSize(math.floor(22 * point_size_scale))
painter.setFont(font)
pen = QPen(QColor("#9782ff"))
painter.setPen(pen)
painter.drawText(
QRect(40, 450, 960, 540),
Splash.COPYRIGHT_STR,
)
# Version
font = painter.font()
font.setPointSize(math.floor(22 * point_size_scale))
painter.setFont(font)
pen = QPen(QColor("#809782ff"))
painter.setPen(pen)
painter.drawText(
QRect(40, 475, 960, 540),
Splash.VERSION_STR,
)
case Splash.SPLASH_95:
# Copyright
font = QFont()
font.setFamily("Times")
font.setPointSize(math.floor(22 * point_size_scale))
font.setWeight(QFont.Weight.DemiBold)
font.setStyleHint(QFont.StyleHint.Serif)
painter.setFont(font)
pen = QPen(QColor("#000000"))
painter.setPen(pen)
painter.drawText(
QRect(88, -25, 960, 540),
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft),
Splash.COPYRIGHT_STR,
)
# Version
font.setPointSize(math.floor(22 * point_size_scale))
painter.setFont(font)
pen = QPen(QColor("#AA2A0044"))
painter.setPen(pen)
painter.drawText(
QRect(-30, 25, 960, 540),
int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight),
Splash.VERSION_STR,
)
case _:
pass
pixmap.setDevicePixelRatio(self.ratio)
pixmap = pixmap.scaledToWidth(
math.floor(
min(
(self.screen_width * self.ratio) / 4,
pixmap.width(),
)
),
Qt.TransformationMode.SmoothTransformation,
)
return pixmap
def _build_splash_screen(self):
"""Build the internal splash screen."""
self.splash_screen = QSplashScreen(self.get_pixmap(), Qt.WindowType.WindowStaysOnTopHint)
def show(self):
"""Show the splash screen."""
if not self.splash_screen:
self._build_splash_screen()
if self.splash_screen:
self.splash_screen.show()
def finish(self, widget: QWidget):
"""Hide the splash screen with this widget is finished displaying."""
if self.splash_screen:
self.splash_screen.finish(widget)

View File

@@ -25,7 +25,6 @@ from PySide6 import QtCore
from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal
from PySide6.QtGui import (
QAction,
QColor,
QDragEnterEvent,
QDragMoveEvent,
QDropEvent,
@@ -33,7 +32,6 @@ from PySide6.QtGui import (
QGuiApplication,
QIcon,
QMouseEvent,
QPixmap,
)
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import (
@@ -46,7 +44,6 @@ from PySide6.QtWidgets import (
QMessageBox,
QPushButton,
QScrollArea,
QSplashScreen,
QWidget,
)
from src.core.constants import (
@@ -84,6 +81,7 @@ from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
from src.qt.modals.folders_to_tags import FoldersToTagsModal
from src.qt.modals.tag_database import TagDatabasePanel
from src.qt.resource_manager import ResourceManager
from src.qt.splash import Splash
from src.qt.translations import Translations
from src.qt.widgets.item_thumb import BadgeType, ItemThumb
from src.qt.widgets.migration_modal import JsonMigrationModal
@@ -219,13 +217,6 @@ class QtDriver(DriverMixin, QObject):
app = QApplication(sys.argv)
app.setStyle("Fusion")
# pal: QPalette = app.palette()
# pal.setColor(QPalette.ColorGroup.Active,
# QPalette.ColorRole.Highlight, QColor('#6E4BCE'))
# pal.setColor(QPalette.ColorGroup.Normal,
# QPalette.ColorRole.Window, QColor('#110F1B'))
# app.setPalette(pal)
# home_path = Path(__file__).parent / "ui/home.ui"
icon_path = Path(__file__).parents[2] / "resources/icon.png"
# Handle OS signals
@@ -243,23 +234,12 @@ class QtDriver(DriverMixin, QObject):
self.main_window.dragMoveEvent = self.drag_move_event # type: ignore[method-assign]
self.main_window.dropEvent = self.drop_event # type: ignore[method-assign]
splash_pixmap = QPixmap(":/images/splash.png")
splash_pixmap.setDevicePixelRatio(self.main_window.devicePixelRatio())
splash_pixmap = splash_pixmap.scaledToWidth(
math.floor(
min(
(
QGuiApplication.primaryScreen().geometry().width()
* self.main_window.devicePixelRatio()
)
/ 4,
splash_pixmap.width(),
)
),
Qt.TransformationMode.SmoothTransformation,
self.splash: Splash = Splash(
resource_manager=self.rm,
screen_width=QGuiApplication.primaryScreen().geometry().width(),
splash_name="", # TODO: Get splash name from config
device_ratio=self.main_window.devicePixelRatio(),
)
self.splash = QSplashScreen(splash_pixmap, Qt.WindowType.WindowStaysOnTopHint)
# self.splash.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.splash.show()
if os.name == "nt":
@@ -533,15 +513,7 @@ class QtDriver(DriverMixin, QObject):
self.migration_modal: JsonMigrationModal = None
path_result = self.evaluate_path(str(self.args.open).lstrip().rstrip())
# check status of library path evaluating
if path_result.success and path_result.library_path:
self.splash.showMessage(
Translations.translate_formatted(
"splash.opening_library", library_path=path_result.library_path
),
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
QColor("#9782ff"),
)
self.open_library(path_result.library_path)
# check ffmpeg and show warning if not