diff --git a/README.md b/README.md index 2d685ee9..955e4c20 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Ruff](https://github.com/TagStudioDev/TagStudio/actions/workflows/ruff.yaml/badge.svg)](https://github.com/TagStudioDev/TagStudio/actions/workflows/ruff.yaml)

- +

TagStudio is a photo & file organization application with an underlying tag-based system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure. **Read the documentation and more at [docs.tagstud.io](https://docs.tagstud.io)!** diff --git a/REUSE.toml b/REUSE.toml index 1897ada1..88839d64 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -33,17 +33,18 @@ SPDX-License-Identifier = "GPL-3.0-or-later" [[annotations]] path = [ + "src/tagstudio/resources/qt/images/bxs-clipboard-regular.png", "src/tagstudio/resources/qt/images/bxs-left-arrow.png", + "src/tagstudio/resources/qt/images/bxs-pencil-solid.png", "src/tagstudio/resources/qt/images/bxs-right-arrow.png", + "src/tagstudio/resources/qt/images/bxs-trash-solid.png", + "src/tagstudio/resources/qt/images/bxs-volume-full-solid.png", "src/tagstudio/resources/qt/images/file_icons/database.png", ] SPDX-FileCopyrightText = "(c) 2026 Boxicons" SPDX-License-Identifier = "MIT" [[annotations]] -path = [ - "src/tagstudio/resources/qt/images/volume.svg", - "src/tagstudio/resources/qt/images/volume_mute.svg", -] +path = ["src/tagstudio/resources/qt/images/dupe_file_stat.png"] SPDX-FileCopyrightText = "(c) github:google/material-design-icons Contributors" SPDX-License-Identifier = "Apache-2.0" diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 00000000..02baad57 Binary files /dev/null and b/docs/assets/favicon.png differ diff --git a/docs/assets/github_header.png b/docs/assets/github_header.png deleted file mode 100644 index 28132f65..00000000 Binary files a/docs/assets/github_header.png and /dev/null differ diff --git a/docs/assets/icon.ico b/docs/assets/icon.ico deleted file mode 100644 index 71335bd5..00000000 Binary files a/docs/assets/icon.ico and /dev/null differ diff --git a/docs/assets/icon.png b/docs/assets/icon.png deleted file mode 100644 index dd3d2772..00000000 Binary files a/docs/assets/icon.png and /dev/null differ diff --git a/docs/assets/icon_mono.svg b/docs/assets/icon_mono.svg index 67060eb0..db328eab 100644 --- a/docs/assets/icon_mono.svg +++ b/docs/assets/icon_mono.svg @@ -1,10 +1,7 @@ - - - - - - + + + diff --git a/docs/assets/tag_bubbles.png b/docs/assets/tag_bubbles.png index c7e7a96c..263e562a 100644 Binary files a/docs/assets/tag_bubbles.png and b/docs/assets/tag_bubbles.png differ diff --git a/docs/assets/tagstudio_logo-text_color.png b/docs/assets/tagstudio_logo-text_color.png deleted file mode 100644 index b6286186..00000000 Binary files a/docs/assets/tagstudio_logo-text_color.png and /dev/null differ diff --git a/docs/assets/tagstudio_logo-text_color.png b/docs/assets/tagstudio_logo-text_color.png new file mode 120000 index 00000000..37ad1503 --- /dev/null +++ b/docs/assets/tagstudio_logo-text_color.png @@ -0,0 +1 @@ +/Users/cyanvoxel/Local/Dev/TagStudio/src/tagstudio/resources/qt/images/tagstudio_logo-text_color.png \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 583e0cd5..5ab6ca02 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,7 +85,7 @@ theme: icon: material/lightbulb-night-outline name: Switch to System Preference logo: assets/icon_mono.svg - favicon: assets/icon.ico + favicon: assets/favicon.png font: code: Jetbrains Mono language: en diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 349d0e9e..7e16199c 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -66,7 +66,7 @@ class TagColorEnum(enum.IntEnum): class ItemType(enum.Enum): ENTRY = 0 - COLLATION = 1 + ENTRY_GROUP = 1 TAG_GROUP = 2 diff --git a/src/tagstudio/qt/global_settings.py b/src/tagstudio/qt/global_settings.py index 94be1f07..e82e9648 100644 --- a/src/tagstudio/qt/global_settings.py +++ b/src/tagstudio/qt/global_settings.py @@ -43,6 +43,7 @@ class Splash(StrEnum): CLASSIC = "classic" GOO_GEARS = "goo_gears" NINETY_FIVE = "95" + AURORA = "aurora" class TomlEnumEncoder(toml.TomlEncoder): diff --git a/src/tagstudio/qt/helpers/color_overlay.py b/src/tagstudio/qt/helpers/color_overlay.py index 0d276210..3a45aee8 100644 --- a/src/tagstudio/qt/helpers/color_overlay.py +++ b/src/tagstudio/qt/helpers/color_overlay.py @@ -16,11 +16,14 @@ _THEME_DARK_BG: str = "#000000DD" _THEME_LIGHT_BG: str = "#FFFFFF55" -def theme_fg_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image: +def theme_fg_overlay( + image: Image.Image, inverse: bool = False, use_alpha: bool = True +) -> Image.Image: """Overlay the foreground theme color onto an image. Args: image (Image): The PIL Image object to apply an overlay to. + inverse (bool): Option inverse the overlay color relative to the current theme. use_alpha (bool): Option to retain the base image's alpha value when applying the overlay. """ dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG @@ -29,6 +32,8 @@ def theme_fg_overlay(image: Image.Image, use_alpha: bool = True) -> Image.Image: overlay_color = ( dark_fg if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark else light_fg ) + if inverse: + overlay_color = light_fg if overlay_color == dark_fg else dark_fg im = Image.new(mode="RGBA", size=image.size, color=overlay_color) return _apply_overlay(image, im) diff --git a/src/tagstudio/qt/mixed/about_modal.py b/src/tagstudio/qt/mixed/about_modal.py index 3d1092f1..059ce54e 100644 --- a/src/tagstudio/qt/mixed/about_modal.py +++ b/src/tagstudio/qt/mixed/about_modal.py @@ -3,6 +3,7 @@ import math +from pathlib import Path from PIL import ImageQt from PySide6.QtCore import Qt @@ -28,7 +29,7 @@ from tagstudio.qt.translations import Translations class AboutModal(QWidget): - def __init__(self, config_path): + def __init__(self, config_path: Path | str): super().__init__() self.setWindowTitle(Translations["about.title"]) @@ -47,8 +48,8 @@ class AboutModal(QWidget): ) self.setWindowModality(Qt.WindowModality.ApplicationModal) - self.setMinimumSize(360, 540) - self.setMaximumSize(600, 600) + self.setMinimumSize(420, 500) + self.setMaximumSize(600, 800) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(0, 12, 0, 0) self.root_layout.setSpacing(0) @@ -59,12 +60,12 @@ class AboutModal(QWidget): self.content_layout.setContentsMargins(12, 12, 12, 12) self.content_layout.setSpacing(12) - # TagStudio Icon Logo -------------------------------------------------- + # TagStudio Logo ------------------------------------------------------- self.logo_widget = QLabel() - self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.get("icon"))) + self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.ts_logo_text_color)) self.logo_pixmap.setDevicePixelRatio(self.devicePixelRatio()) self.logo_pixmap = self.logo_pixmap.scaledToWidth( - math.floor(128 * self.devicePixelRatio()), Qt.TransformationMode.SmoothTransformation + math.floor(384 * self.devicePixelRatio()), Qt.TransformationMode.SmoothTransformation ) self.logo_widget.setPixmap(self.logo_pixmap) self.logo_widget.setContentsMargins(0, 0, 0, 0) @@ -72,7 +73,7 @@ class AboutModal(QWidget): # Title ---------------------------------------------------------------- branch: str = (" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else "" - self.title_label = QLabel(f"

TagStudio Alpha {VERSION}{branch}

") + self.title_label = QLabel(f"

{Translations['about.version']} {VERSION} {branch}

") self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Description ---------------------------------------------------------- @@ -105,7 +106,7 @@ class AboutModal(QWidget): self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) # Version - version_title = QLabel("Version") + version_title = QLabel(Translations["about.version"]) most_recent_release = unwrap(TagStudioCore.get_most_recent_release_version(), "UNKNOWN") version_content_style = self.form_content_style if most_recent_release == VERSION: @@ -174,8 +175,8 @@ class AboutModal(QWidget): self.content_layout.addWidget(self.title_label) self.content_layout.addWidget(self.desc_label) self.content_layout.addWidget(self.system_info_widget) - self.content_layout.addWidget(self.links_label) self.content_layout.addStretch(1) + self.content_layout.addWidget(self.links_label) self.content_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.root_layout.addWidget(self.content_widget) diff --git a/src/tagstudio/qt/mixed/field_widget.py b/src/tagstudio/qt/mixed/field_widget.py index 04ee3c37..60face98 100644 --- a/src/tagstudio/qt/mixed/field_widget.py +++ b/src/tagstudio/qt/mixed/field_widget.py @@ -2,39 +2,28 @@ # SPDX-License-Identifier: GPL-3.0-only -import math from collections.abc import Callable -from pathlib import Path from typing import override from warnings import catch_warnings import structlog -from PIL import Image, ImageQt -from PySide6.QtCore import QEvent, Qt +from PIL import ImageQt +from PySide6.QtCore import QEvent, QSize, Qt from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget from tagstudio.core.enums import Theme +from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.resource_manager import ResourceManager logger = structlog.get_logger(__name__) class FieldContainer(QWidget): - # TODO: reference a resources folder rather than path.parents[2]? - clipboard_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/clipboard_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - clipboard_icon_128.load() - - edit_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/edit_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - edit_icon_128.load() - - trash_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/trash_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - trash_icon_128.load() + rm: ResourceManager = ResourceManager() + copy_icon = theme_fg_overlay(rm.copy, inverse=True) + edit_icon = theme_fg_overlay(rm.edit, inverse=True) + trash_icon = theme_fg_overlay(rm.trash, inverse=True) # TODO: There should be a global button theme somewhere. container_style = ( @@ -93,7 +82,8 @@ class FieldContainer(QWidget): self.copy_button.setMinimumSize(button_size, button_size) self.copy_button.setMaximumSize(button_size, button_size) self.copy_button.setFlat(True) - self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128))) + self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.copy_icon))) + self.copy_button.setIconSize(QSize(20, 20)) self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor) self.title_layout.addWidget(self.copy_button) self.copy_button.setHidden(True) @@ -103,7 +93,8 @@ class FieldContainer(QWidget): self.edit_button.setMinimumSize(button_size, button_size) self.edit_button.setMaximumSize(button_size, button_size) self.edit_button.setFlat(True) - self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128))) + self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.edit_icon))) + self.edit_button.setIconSize(QSize(20, 20)) self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor) self.title_layout.addWidget(self.edit_button) self.edit_button.setHidden(True) @@ -113,7 +104,8 @@ class FieldContainer(QWidget): self.remove_button.setMinimumSize(button_size, button_size) self.remove_button.setMaximumSize(button_size, button_size) self.remove_button.setFlat(True) - self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128))) + self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(FieldContainer.trash_icon))) + self.remove_button.setIconSize(QSize(20, 20)) self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor) self.title_layout.addWidget(self.remove_button) self.remove_button.setHidden(True) diff --git a/src/tagstudio/qt/mixed/item_thumb.py b/src/tagstudio/qt/mixed/item_thumb.py index 81223316..8cbd361c 100644 --- a/src/tagstudio/qt/mixed/item_thumb.py +++ b/src/tagstudio/qt/mixed/item_thumb.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import TYPE_CHECKING, override import structlog -from PIL import Image, ImageQt from PySide6.QtCore import QEvent, QMimeData, QSize, Qt, QUrl from PySide6.QtGui import QAction, QDrag, QEnterEvent, QGuiApplication, QMouseEvent, QPixmap from PySide6.QtWidgets import QBoxLayout, QCheckBox, QHBoxLayout, QLabel, QVBoxLayout, QWidget @@ -62,17 +61,7 @@ def badge_update_lock(func): class ItemThumb(FlowWidget): - """The thumbnail widget for a library item (Entry, Collation, Tag Group, etc.).""" - - collation_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/collation_icon_128.png") - ) - collation_icon_128.load() - - tag_group_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/tag_group_icon_128.png") - ) - tag_group_icon_128.load() + """The thumbnail widget for a library item (Entry, Entry Group, etc.).""" small_text_style = ( "background-color:rgba(0, 0, 0, 192);" @@ -141,8 +130,8 @@ class ItemThumb(FlowWidget): # | ARC FAV| Top Right: Favorite & Archived Badges # | | # | | - # |EXT #| Lower Left: File Type, Tag Group Icon, or Collation Icon - # +----------+ Lower Right: Collation Count, Video Length, or Word Count + # |EXT #| Lower Left: File Type or Entry Group Icon + # +----------+ Lower Right: Entry Group Count, Video Length, or Word Count # # Filename Underneath: (Optional) Filename @@ -221,19 +210,19 @@ class ItemThumb(FlowWidget): # Static Badges ======================================================== # Item Type Badge ------------------------------------------------------ - # Used for showing the Tag Group / Collation icons. + # Used for showing the Entry Group icons. # Mutually exclusive with the File Extension Badge. self.item_type_badge = QLabel() self.item_type_badge.setObjectName("itemBadge") - self.item_type_badge.setPixmap( - QPixmap.fromImage( - ImageQt.ImageQt( - ItemThumb.collation_icon_128.resize( - (check_size, check_size), Image.Resampling.BILINEAR - ) - ) - ) - ) + # self.item_type_badge.setPixmap( + # QPixmap.fromImage( + # ImageQt.ImageQt( + # ItemThumb.collation_icon_128.resize( + # (check_size, check_size), Image.Resampling.BILINEAR + # ) + # ) + # ) + # ) self.item_type_badge.setMinimumSize(check_size, check_size) self.item_type_badge.setMaximumSize(check_size, check_size) self.bottom_layout.addWidget(self.item_type_badge) @@ -247,7 +236,7 @@ class ItemThumb(FlowWidget): self.bottom_layout.addStretch(2) # Count Badge ---------------------------------------------------------- - # Used for Tag Group + Collation counts, video length, word count, etc. + # Used for Entry Group counts, video length, word count, etc. self.count_badge = QLabel() self.count_badge.setObjectName("countBadge") self.count_badge.setText("-:--") @@ -343,7 +332,7 @@ class ItemThumb(FlowWidget): self.count_badge.setStyleSheet(ItemThumb.small_text_style) self.count_badge.setHidden(True) self.ext_badge.setHidden(True) - elif mode == ItemType.COLLATION: + elif mode == ItemType.ENTRY_GROUP: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False) self.thumb_button.setCursor(Qt.CursorShape.PointingHandCursor) self.thumb_button.setHidden(False) @@ -352,13 +341,6 @@ class ItemThumb(FlowWidget): self.count_badge.setStyleSheet(ItemThumb.med_text_style) self.count_badge.setHidden(False) self.item_type_badge.setHidden(False) - elif mode == ItemType.TAG_GROUP: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, on=False) - self.thumb_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.thumb_button.setHidden(False) - self.ext_badge.setHidden(True) - self.count_badge.setHidden(False) - self.item_type_badge.setHidden(False) self.mode = mode def set_extension(self, filename: Path) -> None: diff --git a/src/tagstudio/qt/mixed/landing.py b/src/tagstudio/qt/mixed/landing.py index 7f5f6ba7..f35a9b47 100644 --- a/src/tagstudio/qt/mixed/landing.py +++ b/src/tagstudio/qt/mixed/landing.py @@ -4,7 +4,6 @@ import sys import typing -from pathlib import Path import structlog from PIL import Image, ImageQt @@ -12,7 +11,8 @@ from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget -from tagstudio.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay +from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations from tagstudio.qt.views.clickable_label import ClickableLabel @@ -24,6 +24,10 @@ logger = structlog.get_logger(__name__) class LandingWidget(QWidget): + rm: ResourceManager = ResourceManager() + mono_logo: Image.Image = rm.ts_logo_text_mono + color_logo: Image.Image = rm.ts_logo_text_color + def __init__(self, driver: "QtDriver", pixel_ratio: float): super().__init__() self.driver = driver @@ -39,10 +43,6 @@ class LandingWidget(QWidget): self.setLayout(self.landing_layout) # Create landing logo -------------------------------------------------- - # self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png") - self.logo_raw: Image.Image = Image.open( - Path(__file__).parents[2] / "resources/qt/images/tagstudio_logo_text_mono.png" - ) self.landing_pixmap: QPixmap = QPixmap() self.update_logo_color() self.logo_label.clicked.connect(self._update_special_click) @@ -58,9 +58,9 @@ class LandingWidget(QWidget): # Create "Open/Create Library" button ---------------------------------- if sys.platform == "darwin": - open_shortcut_text = "(⌘+O)" + open_shortcut_text = "(⌘ + O)" else: - open_shortcut_text = "(Ctrl+O)" + open_shortcut_text = "(Ctrl + O)" self.open_button: QPushButton = QPushButton( Translations.format("landing.open_create_library", shortcut=open_shortcut_text) ) @@ -83,21 +83,22 @@ class LandingWidget(QWidget): self.landing_layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignCenter) self.landing_layout.addWidget(self.status_label, alignment=Qt.AlignmentFlag.AlignCenter) - def update_logo_color(self, style: typing.Literal["mono", "gradient"] = "mono"): + def update_logo_color(self, style: typing.Literal["mono", "color"] = "mono"): """Update the color of the TagStudio logo. Args: - style (str): = The style of the logo. Either "mono" or "gradient". + style (str): = The style of the logo. Either "mono" or "color". """ if style == "mono": - logo_im = theme_fg_overlay(self.logo_raw) - elif style == "gradient": - gradient_colors: list[str] = ["#d27bf4", "#7992f5", "#63c6e3", "#63f5cf"] - logo_im = gradient_overlay(self.logo_raw, gradient_colors) + logo_im = theme_fg_overlay(LandingWidget.mono_logo) + elif style == "color": + logo_im = LandingWidget.color_logo - logo_final: Image.Image = Image.new(mode="RGBA", size=self.logo_raw.size, color="#00000000") + logo_final: Image.Image = Image.new( + mode="RGBA", size=LandingWidget.mono_logo.size, color="#00000000" + ) - logo_final.paste(logo_im, (0, 0), mask=self.logo_raw) + logo_final.paste(logo_im, (0, 0), mask=LandingWidget.mono_logo) self.landing_pixmap = QPixmap.fromImage(ImageQt.ImageQt(logo_im)) self.landing_pixmap.setDevicePixelRatio(self._pixel_ratio) @@ -105,7 +106,10 @@ class LandingWidget(QWidget): self._logo_width, Qt.TransformationMode.SmoothTransformation ) self.logo_label.setMaximumHeight( - int(self.logo_raw.size[1] * (self.logo_raw.size[0] / self._logo_width)) + int( + LandingWidget.mono_logo.size[1] + * (LandingWidget.mono_logo.size[0] / self._logo_width) + ) ) self.logo_label.setMaximumWidth(self._logo_width) self.logo_label.setPixmap(self.landing_pixmap) @@ -119,17 +123,13 @@ class LandingWidget(QWidget): if self._special_click_count >= 0: self._special_click_count += 1 if self._special_click_count >= 10: - self.update_logo_color("gradient") + self.update_logo_color("color") self.animate_logo_pop() self._special_click_count = -1 def animate_logo_in(self): - """Animate in the TagStudio logo.""" - # NOTE: Sometimes, mostly on startup without a library open, the - # y position of logo_label is something like 10. I'm not sure what - # the cause of this is, so I've just done this workaround to disable - # the animation if the y position is too incorrect. - if self.logo_label.y() > 50: + """Animate the TagStudio logo in, if not opening a library on start.""" + if not self.driver.settings.open_last_loaded_on_startup and not self.driver.args.open: self.logo_pos_anim.setStartValue(QPoint(self.logo_label.x(), self.logo_label.y() - 100)) self.logo_pos_anim.setEndValue(self.logo_label.pos()) self.logo_pos_anim.start() diff --git a/src/tagstudio/qt/mixed/media_player.py b/src/tagstudio/qt/mixed/media_player.py index c180964a..878ab7fb 100644 --- a/src/tagstudio/qt/mixed/media_player.py +++ b/src/tagstudio/qt/mixed/media_player.py @@ -5,10 +5,10 @@ import typing from pathlib import Path from time import gmtime -from typing import cast, override +from typing import override import structlog -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageQt from PySide6.QtCore import QEvent, QObject, QRectF, QSize, Qt, QUrl, QVariantAnimation from PySide6.QtGui import ( QAction, @@ -18,12 +18,12 @@ from PySide6.QtGui import ( QLinearGradient, QMouseEvent, QPen, + QPixmap, QRegion, QResizeEvent, ) from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer from PySide6.QtMultimediaWidgets import QGraphicsVideoItem -from PySide6.QtSvgWidgets import QSvgWidget from PySide6.QtWidgets import ( QGraphicsScene, QGraphicsView, @@ -31,10 +31,12 @@ from PySide6.QtWidgets import ( QLabel, QSizePolicy, QSlider, + QToolButton, QVBoxLayout, QWidget, ) +from tagstudio.qt.helpers.color_overlay import theme_fg_overlay from tagstudio.qt.translations import Translations from tagstudio.qt.views.clickable_slider import ClickableSlider @@ -55,6 +57,18 @@ class MediaPlayer(QGraphicsView): def __init__(self, driver: "QtDriver") -> None: super().__init__() self.driver = driver + self.play_icon = QPixmap.fromImage( + ImageQt.ImageQt(theme_fg_overlay(self.driver.rm.bxs_right_arrow, use_alpha=False)) + ) + self.pause_icon = QPixmap.fromImage( + ImageQt.ImageQt(theme_fg_overlay(self.driver.rm.pause_icon, use_alpha=False)) + ) + self.mute_icon = QPixmap.fromImage( + ImageQt.ImageQt(theme_fg_overlay(self.driver.rm.mute_icon, use_alpha=False)) + ) + self.volume_icon = QPixmap.fromImage( + ImageQt.ImageQt(theme_fg_overlay(self.driver.rm.volume_icon, use_alpha=False)) + ) slider_style = """ QSlider { @@ -169,27 +183,27 @@ class MediaPlayer(QGraphicsView): self.sub_controls.setStyleSheet("background: transparent;") self.sub_controls.setMinimumHeight(16) - self.play_pause = QSvgWidget() + self.play_pause = QToolButton() + self.play_pause.setStyleSheet("QToolButton { border: none; background: transparent; }") self.play_pause.setCursor(Qt.CursorShape.PointingHandCursor) - self.play_pause.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True) - self.play_pause.setMouseTracking(True) - self.play_pause.installEventFilter(self) + self.play_pause.clicked.connect(self.toggle_play) self.load_toggle_play_icon(playing=False) - self.play_pause.resize(16, 16) + self.play_pause.setIconSize(QSize(20, 20)) + self.play_pause.resize(20, 20) self.play_pause.setSizePolicy(fixed_policy) - self.play_pause.setStyleSheet("background: transparent;") self.play_pause.hide() sub_layout.addWidget(self.play_pause) sub_layout.setAlignment(self.play_pause, Qt.AlignmentFlag.AlignLeft) - self.mute_unmute = QSvgWidget() + self.mute_unmute = QToolButton() + self.mute_unmute.setStyleSheet("QToolButton { border: none; background: transparent; }") self.mute_unmute.setCursor(Qt.CursorShape.PointingHandCursor) - self.mute_unmute.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, on=True) - self.mute_unmute.setMouseTracking(True) + self.mute_unmute.clicked.connect(self.toggle_mute) self.mute_unmute.installEventFilter(self) self.load_mute_unmute_icon(muted=False) - self.mute_unmute.resize(16, 16) + self.mute_unmute.setIconSize(QSize(20, 20)) + self.mute_unmute.resize(20, 20) self.mute_unmute.setSizePolicy(fixed_policy) self.mute_unmute.hide() @@ -210,7 +224,7 @@ class MediaPlayer(QGraphicsView): sub_layout.addStretch() self.position_label = QLabel("0:00") - self.position_label.setStyleSheet("color: white;") + self.position_label.setStyleSheet("color: white; font-family: Oxanium; font-weight:bold;") sub_layout.addWidget(self.position_label) root_layout.setAlignment(self.position_label, Qt.AlignmentFlag.AlignRight) self.position_label.hide() @@ -253,21 +267,10 @@ class MediaPlayer(QGraphicsView): """Apply a rounded corner effect to the video player.""" width: int = int(max(self.contentsRect().size().width(), 0)) height: int = int(max(self.contentsRect().size().height(), 0)) - mask = Image.new( - "RGBA", - ( - width, - height, - ), - (0, 0, 0, 255), - ) + mask = Image.new("RGBA", (width, height), (0, 0, 0, 255)) draw = ImageDraw.Draw(mask) - draw.rounded_rectangle( - (0, 0) + (width, height), - radius=8, - fill=(0, 0, 0, 0), - ) - final_mask = mask.getchannel("A").toqpixmap() + draw.rounded_rectangle((0, 0) + (width, height), radius=8, fill=(0, 0, 0, 0)) + final_mask: QPixmap = mask.getchannel("A").toqpixmap() # pyright: ignore[reportUnknownVariableType] self.setMask(QRegion(QBitmap(final_mask))) def set_tint_opacity(self, opacity: int) -> None: @@ -322,15 +325,7 @@ class MediaPlayer(QGraphicsView): @override def eventFilter(self, arg__1: QObject, arg__2: QEvent) -> bool: """Manage events for the media player.""" - if ( - arg__2.type() == QEvent.Type.MouseButtonPress - and arg__2.button() == Qt.MouseButton.LeftButton # pyright: ignore[reportAttributeAccessIssue] - ): - if arg__1 == self.play_pause: - self.toggle_play() - elif arg__1 == self.mute_unmute: - self.toggle_mute() - elif arg__2.type() is QEvent.Type.Enter: + if arg__2.type() is QEvent.Type.Enter: if arg__1 == self or arg__1 == self.video_preview: self.underMouse() elif arg__1 == self.mute_unmute: @@ -414,12 +409,12 @@ class MediaPlayer(QGraphicsView): self.player.play() def load_toggle_play_icon(self, playing: bool) -> None: - icon = cast(bytes, self.driver.rm.pause_icon if playing else self.driver.rm.play_icon) - self.play_pause.load(icon) + icon = self.pause_icon if playing else self.play_icon + self.play_pause.setIcon(icon) def load_mute_unmute_icon(self, muted: bool) -> None: - icon = cast(bytes, self.driver.rm.volume_mute_icon if muted else self.driver.rm.volume_icon) - self.mute_unmute.load(icon) + icon = self.mute_icon if muted else self.volume_icon + self.mute_unmute.setIcon(icon) def slider_value_changed(self, value: int) -> None: if self.timeline_slider.isSliderDown(): diff --git a/src/tagstudio/qt/mixed/pagination.py b/src/tagstudio/qt/mixed/pagination.py index a1e77c1c..c03d2b57 100644 --- a/src/tagstudio/qt/mixed/pagination.py +++ b/src/tagstudio/qt/mixed/pagination.py @@ -44,7 +44,7 @@ class Pagination(QWidget): # [<] ---------------------------------- self.prev_button = QPushButtonWrapper() - prev_icon: Image.Image = self.rm.get("bxs-left-arrow") # pyright: ignore[reportAssignmentType] + prev_icon: Image.Image = self.rm.bxs_left_arrow prev_icon = theme_fg_overlay(prev_icon, use_alpha=False) self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon))) self.prev_button.setIconSize(QSize(12, 12)) @@ -97,7 +97,7 @@ class Pagination(QWidget): # ---------------------------------- [>] self.next_button = QPushButtonWrapper() - next_icon: Image.Image = self.rm.get("bxs-right-arrow") # pyright: ignore[reportAssignmentType] + next_icon: Image.Image = self.rm.bxs_right_arrow next_icon = theme_fg_overlay(next_icon, use_alpha=False) self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon))) self.next_button.setIconSize(QSize(12, 12)) diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py index 5e8c022d..fe76f83d 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -56,6 +56,7 @@ class SettingsPanel(PanelWidget): Splash.CLASSIC: Translations["settings.splash.option.classic"], Splash.GOO_GEARS: Translations["settings.splash.option.goo_gears"], Splash.NINETY_FIVE: Translations["settings.splash.option.ninety_five"], + Splash.AURORA: Translations["settings.splash.option.aurora"], } tag_click_action_map: dict[TagClickActionOption, str] = { diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index 866e3df3..492247ed 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -449,11 +449,10 @@ class ThumbRenderer(QObject): ) # Get icon by name - icon: Image.Image | None = self.rm.get(name) # pyright: ignore[reportAssignmentType] + icon = self.rm.get(name) + assert isinstance(icon, Image.Image) or icon is None if not icon: - icon = self.rm.get("file_generic") # pyright: ignore[reportAssignmentType] - if not icon: - icon = Image.new(mode="RGBA", size=(32, 32), color="magenta") + icon = self.rm.file_generic # Resize icon to fit icon_ratio icon = icon.resize((math.ceil(size[0] // icon_ratio), math.ceil(size[1] // icon_ratio))) @@ -547,11 +546,10 @@ class ThumbRenderer(QObject): ) # Get icon by name - icon: Image.Image | None = self.rm.get(name) # pyright: ignore[reportAssignmentType] + icon = self.rm.get(name) + assert isinstance(icon, Image.Image) if not icon: - icon = self.rm.get("file_generic") # pyright: ignore[reportAssignmentType] - if not icon: - icon = Image.new(mode="RGBA", size=(32, 32), color="magenta") + icon = self.rm.file_generic # Resize icon to fit icon_ratio icon = icon.resize( @@ -1597,7 +1595,7 @@ class ThumbRenderer(QObject): padding_factor = 18 im_ = im - icon: Image.Image = self.rm.get("ignored") # pyright: ignore[reportAssignmentType] + icon: Image.Image = self.rm.ignored icon = icon.resize( ( diff --git a/src/tagstudio/qt/resource_manager.py b/src/tagstudio/qt/resource_manager.py index f1d387ab..d769a4ce 100644 --- a/src/tagstudio/qt/resource_manager.py +++ b/src/tagstudio/qt/resource_manager.py @@ -3,31 +3,23 @@ from pathlib import Path -from typing import Literal, TypedDict import structlog import ujson -from PIL import Image, ImageFile +from PIL import Image from PySide6.QtGui import QPixmap logger = structlog.get_logger(__name__) -class TResourceJsonAttrDict(TypedDict): - path: str - mode: Literal["qpixmap", "pil", "rb", "r"] - - -TData = bytes | str | ImageFile.ImageFile | QPixmap - RESOURCE_FOLDER: Path = Path(__file__).parents[1] class ResourceManager: """A resource manager for retrieving resources.""" - _map: dict[str, TResourceJsonAttrDict] = {} - _cache: dict[str, TData] = {} + _map: dict[str, dict[str, str]] = {} + _cache: dict[str, bytes | str | Image.Image | QPixmap] = {} _instance: "ResourceManager | None" = None def __new__(cls): @@ -43,7 +35,7 @@ class ResourceManager: return ResourceManager._instance @staticmethod - def get_path(id: str) -> Path | None: + def get_path(id: str): """Get a resource's path from the ResourceManager. Args: @@ -52,12 +44,21 @@ class ResourceManager: Returns: Path: The resource path if found, else None. """ - res: TResourceJsonAttrDict | None = ResourceManager._map.get(id) - if res is not None: - return RESOURCE_FOLDER / "resources" / res.get("path") - return None + try: + res = ResourceManager._map.get(id) + if res is None: + raise AttributeError + resource_path = res.get("path") + if resource_path is None: + raise FileNotFoundError - def get(self, id: str) -> TData | None: + except (FileNotFoundError, AttributeError) as e: + logger.error("[ResourceManager]: Could not find path for resource: ", id=str, error=e) + return None + + return RESOURCE_FOLDER / "resources" / resource_path + + def get(self, id: str): """Get a resource from the ResourceManager. Args: @@ -70,42 +71,51 @@ class ResourceManager: QPixmap: When the data is in PySide6.QtGui.QPixmap format. None: If resource couldn't load. """ - cached_res: TData | None = ResourceManager._cache.get(id) + cached_res = ResourceManager._cache.get(id) if cached_res is not None: return cached_res else: - res: TResourceJsonAttrDict | None = ResourceManager._map.get(id) - if res is None: + res: dict[str, str] | None = ResourceManager._map.get(id) + + try: + if res is None: + raise AttributeError + resource_path = res.get("path") + if resource_path is None: + raise FileNotFoundError + except (FileNotFoundError, AttributeError) as e: + logger.error("[ResourceManager]: Could not find resource", id=id, error=e) return None - file_path: Path = RESOURCE_FOLDER / "resources" / res.get("path") + file_path = RESOURCE_FOLDER / "resources" / resource_path mode = res.get("mode") - data: TData | None = None + data = None try: match mode: case "r": data = file_path.read_text() - case "rb": data = file_path.read_bytes() - case "pil": data = Image.open(file_path) data.load() - case "qpixmap": data = QPixmap(file_path.as_posix()) + case _: + raise AttributeError - except FileNotFoundError: - logger.error("[ResourceManager][ERROR]: Could not find resource: ", path=file_path) + except (FileNotFoundError, AttributeError) as e: + logger.error( + "[ResourceManager]: Could not find resource", path=file_path, id=id, error=e + ) + return None - if data is not None: - ResourceManager._cache[id] = data + ResourceManager._cache[id] = data return data - def __getattr__(self, __name: str) -> TData: + def __getattr__(self, __name: str): attr = self.get(__name) if attr is not None: return attr diff --git a/src/tagstudio/qt/resource_manager.pyi b/src/tagstudio/qt/resource_manager.pyi new file mode 100644 index 00000000..04872140 --- /dev/null +++ b/src/tagstudio/qt/resource_manager.pyi @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + +from collections.abc import Callable +from pathlib import Path + +from PIL import Image +from PySide6.QtGui import QPixmap + +class ResourceManager: + # Methods + get: Callable[..., Image.Image | bytes | None] + get_path: Callable[..., Path | None] + + # Attributes + _map: dict[str, dict[str, str]] + _cache: dict[str, bytes | str | Image.Image | QPixmap] + _instance: ResourceManager | None + + # Resources IDs from "resources.json" + adobe_illustrator: Image.Image + adobe_photoshop: Image.Image + affinity_photo: Image.Image + archive: Image.Image + audio: Image.Image + broken_link_icon: Image.Image + bxs_left_arrow: Image.Image + bxs_right_arrow: Image.Image + copy: Image.Image + database: Image.Image + document: Image.Image + dupe_file_stat: Image.Image + ebook: Image.Image + edit: Image.Image + file_generic: Image.Image + font: Image.Image + icon: Image.Image + ignored_stat: Image.Image + ignored: Image.Image + image_vector: Image.Image + image: Image.Image + material: Image.Image + model: Image.Image + mute_icon: Image.Image + pause_icon: Image.Image + presentation: Image.Image + program: Image.Image + shader: Image.Image + shortcut: Image.Image + splash_95: QPixmap + splash_aurora: QPixmap + splash_classic: QPixmap + splash_goo_gears: QPixmap + spreadsheet: Image.Image + text: Image.Image + thumb_loading: Image.Image + trash: Image.Image + ts_logo_text_color: Image.Image + ts_logo_text_mono: Image.Image + unlinked_stat: Image.Image + video: Image.Image + volume_icon: Image.Image diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index f7bae77c..5ec05203 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -1,150 +1,170 @@ { - "splash_classic": { - "path": "qt/images/splash/classic.png", - "mode": "qpixmap" - }, - "splash_goo_gears": { - "path": "qt/images/splash/goo_gears.png", - "mode": "qpixmap" - }, - "splash_95": { - "path": "qt/images/splash/95.png", - "mode": "qpixmap" - }, - "icon": { - "path": "icon.png", - "mode": "pil" - }, - "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" - }, - "broken_link_icon": { - "path": "qt/images/broken_link_icon.png", - "mode": "pil" - }, - "ignored": { - "path": "qt/images/ignored_128.png", - "mode": "pil" - }, "adobe_illustrator": { - "path": "qt/images/file_icons/adobe_illustrator.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/adobe_illustrator.png" }, "adobe_photoshop": { - "path": "qt/images/file_icons/adobe_photoshop.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/adobe_photoshop.png" }, "affinity_photo": { - "path": "qt/images/file_icons/affinity_photo.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/affinity_photo.png" }, "archive": { - "path": "qt/images/file_icons/archive.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/archive.png" }, "audio": { - "path": "qt/images/file_icons/audio.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/audio.png" + }, + "broken_link_icon": { + "mode": "pil", + "path": "qt/images/broken_link_icon.png" + }, + "bxs_left_arrow": { + "mode": "pil", + "path": "qt/images/bxs-left-arrow.png" + }, + "bxs_right_arrow": { + "mode": "pil", + "path": "qt/images/bxs-right-arrow.png" + }, + "copy": { + "mode": "pil", + "path": "qt/images/bxs-clipboard-regular.png" }, "database": { - "path": "qt/images/file_icons/database.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/database.png" }, "document": { - "path": "qt/images/file_icons/document.png", - "mode": "pil" - }, - "ebook": { - "path": "qt/images/file_icons/ebook.png", - "mode": "pil" - }, - "file_generic": { - "path": "qt/images/file_icons/file_generic.png", - "mode": "pil" - }, - "font": { - "path": "qt/images/file_icons/font.png", - "mode": "pil" - }, - "image": { - "path": "qt/images/file_icons/image.png", - "mode": "pil" - }, - "image_vector": { - "path": "qt/images/file_icons/image_vector.png", - "mode": "pil" - }, - "material": { - "path": "qt/images/file_icons/material.png", - "mode": "pil" - }, - "model": { - "path": "qt/images/file_icons/model.png", - "mode": "pil" - }, - "presentation": { - "path": "qt/images/file_icons/presentation.png", - "mode": "pil" - }, - "program": { - "path": "qt/images/file_icons/program.png", - "mode": "pil" - }, - "shader": { - "path": "qt/images/file_icons/shader.png", - "mode": "pil" - }, - "shortcut": { - "path": "qt/images/file_icons/shortcut.png", - "mode": "pil" - }, - "spreadsheet": { - "path": "qt/images/file_icons/spreadsheet.png", - "mode": "pil" - }, - "text": { - "path": "qt/images/file_icons/text.png", - "mode": "pil" - }, - "video": { - "path": "qt/images/file_icons/video.png", - "mode": "pil" - }, - "thumb_loading": { - "path": "qt/images/thumb_loading.png", - "mode": "pil" - }, - "bxs-left-arrow": { - "path": "qt/images/bxs-left-arrow.png", - "mode": "pil" - }, - "bxs-right-arrow": { - "path": "qt/images/bxs-right-arrow.png", - "mode": "pil" - }, - "unlinked_stat": { - "path": "qt/images/unlinked_stat.png", - "mode": "pil" - }, - "ignored_stat": { - "path": "qt/images/ignored_stat.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/file_icons/document.png" }, "dupe_file_stat": { - "path": "qt/images/dupe_file_stat.png", - "mode": "pil" + "mode": "pil", + "path": "qt/images/dupe_file_stat.png" + }, + "ebook": { + "mode": "pil", + "path": "qt/images/file_icons/ebook.png" + }, + "edit": { + "mode": "pil", + "path": "qt/images/bxs-pencil-solid.png" + }, + "file_generic": { + "mode": "pil", + "path": "qt/images/file_icons/file_generic.png" + }, + "font": { + "mode": "pil", + "path": "qt/images/file_icons/font.png" + }, + "icon": { + "mode": "pil", + "path": "icon.png" + }, + "ignored": { + "mode": "pil", + "path": "qt/images/ignored_128.png" + }, + "ignored_stat": { + "mode": "pil", + "path": "qt/images/ignored_stat.png" + }, + "image": { + "mode": "pil", + "path": "qt/images/file_icons/image.png" + }, + "image_vector": { + "mode": "pil", + "path": "qt/images/file_icons/image_vector.png" + }, + "material": { + "mode": "pil", + "path": "qt/images/file_icons/material.png" + }, + "model": { + "mode": "pil", + "path": "qt/images/file_icons/model.png" + }, + "presentation": { + "mode": "pil", + "path": "qt/images/file_icons/presentation.png" + }, + "program": { + "mode": "pil", + "path": "qt/images/file_icons/program.png" + }, + "shader": { + "mode": "pil", + "path": "qt/images/file_icons/shader.png" + }, + "shortcut": { + "mode": "pil", + "path": "qt/images/file_icons/shortcut.png" + }, + "splash_95": { + "mode": "qpixmap", + "path": "qt/images/splash/95.png" + }, + "splash_aurora": { + "mode": "qpixmap", + "path": "qt/images/splash/aurora.png" + }, + "splash_classic": { + "mode": "qpixmap", + "path": "qt/images/splash/classic.png" + }, + "splash_goo_gears": { + "mode": "qpixmap", + "path": "qt/images/splash/goo_gears.png" + }, + "spreadsheet": { + "mode": "pil", + "path": "qt/images/file_icons/spreadsheet.png" + }, + "text": { + "mode": "pil", + "path": "qt/images/file_icons/text.png" + }, + "thumb_loading": { + "mode": "pil", + "path": "qt/images/thumb_loading.png" + }, + "trash": { + "mode": "pil", + "path": "qt/images/bxs-trash-solid.png" + }, + "ts_logo_text_color": { + "mode": "pil", + "path": "qt/images/tagstudio_logo-text_color.png" + }, + "ts_logo_text_mono": { + "mode": "pil", + "path": "qt/images/tagstudio_logo-text_mono.png" + }, + "unlinked_stat": { + "mode": "pil", + "path": "qt/images/unlinked_stat.png" + }, + "video": { + "mode": "pil", + "path": "qt/images/file_icons/video.png" + }, + "volume_icon": { + "mode": "pil", + "path": "qt/images/bxs-volume-full-solid.png" + }, + "mute_icon": { + "mode": "pil", + "path": "qt/images/bxs-volume-mute-solid.png" + }, + "pause_icon": { + "mode": "pil", + "path": "qt/images/pause.png" } } diff --git a/src/tagstudio/qt/resources.qrc b/src/tagstudio/qt/resources.qrc index fcaf44c8..3d5b5ab2 100644 --- a/src/tagstudio/qt/resources.qrc +++ b/src/tagstudio/qt/resources.qrc @@ -7,8 +7,5 @@ ../../resources/qt/images/star_icon_filled_128.png ../../resources/qt/images/box_icon_empty_128.png ../../resources/qt/images/box_icon_filled_128.png - - - diff --git a/src/tagstudio/qt/views/library_info_window_view.py b/src/tagstudio/qt/views/library_info_window_view.py index bfd06c0a..8d72fb92 100644 --- a/src/tagstudio/qt/views/library_info_window_view.py +++ b/src/tagstudio/qt/views/library_info_window_view.py @@ -268,7 +268,7 @@ class LibraryInfoWindowView(QWidget): ) self.unlinked_icon = QLabel() - unlinked_image: Image.Image = self.driver.rm.get("unlinked_stat") # pyright: ignore[reportAssignmentType] + unlinked_image: Image.Image = self.driver.rm.unlinked_stat unlinked_pixmap = QPixmap.fromImage(ImageQt.ImageQt(unlinked_image)) unlinked_pixmap.setDevicePixelRatio(self.devicePixelRatio()) unlinked_pixmap = unlinked_pixmap.scaledToWidth( @@ -278,7 +278,7 @@ class LibraryInfoWindowView(QWidget): self.unlinked_icon.setPixmap(unlinked_pixmap) self.ignored_icon = QLabel() - ignored_image: Image.Image = self.driver.rm.get("ignored_stat") # pyright: ignore[reportAssignmentType] + ignored_image: Image.Image = self.driver.rm.ignored_stat ignored_pixmap = QPixmap.fromImage(ImageQt.ImageQt(ignored_image)) ignored_pixmap.setDevicePixelRatio(self.devicePixelRatio()) ignored_pixmap = ignored_pixmap.scaledToWidth( @@ -288,7 +288,7 @@ class LibraryInfoWindowView(QWidget): self.ignored_icon.setPixmap(ignored_pixmap) self.dupe_file_icon = QLabel() - dupe_file_image: Image.Image = self.driver.rm.get("dupe_file_stat") # pyright: ignore[reportAssignmentType] + dupe_file_image: Image.Image = self.driver.rm.dupe_file_stat dupe_file_pixmap = QPixmap.fromImage( ImageQt.ImageQt(theme_fg_overlay(dupe_file_image, use_alpha=False)) ) diff --git a/src/tagstudio/qt/views/main_window.py b/src/tagstudio/qt/views/main_window.py index b9229fdd..ad991c07 100644 --- a/src/tagstudio/qt/views/main_window.py +++ b/src/tagstudio/qt/views/main_window.py @@ -547,7 +547,7 @@ class MainWindow(QMainWindow): self.search_bar_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize) self.back_button = QPushButton(self.central_widget) - back_icon: Image.Image = self.rm.get("bxs-left-arrow") # pyright: ignore[reportAssignmentType] + back_icon: Image.Image = self.rm.bxs_left_arrow back_icon = theme_fg_overlay(back_icon, use_alpha=False) self.back_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(back_icon))) self.back_button.setObjectName("back_button") @@ -556,7 +556,7 @@ class MainWindow(QMainWindow): self.search_bar_layout.addWidget(self.back_button) self.forward_button = QPushButton(self.central_widget) - forward_icon: Image.Image = self.rm.get("bxs-right-arrow") # pyright: ignore[reportAssignmentType] + forward_icon: Image.Image = self.rm.bxs_right_arrow forward_icon = theme_fg_overlay(forward_icon, use_alpha=False) self.forward_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(forward_icon))) self.forward_button.setIconSize(QSize(16, 16)) diff --git a/src/tagstudio/qt/views/splash.py b/src/tagstudio/qt/views/splash.py index 220c4c5c..a71e5bc8 100644 --- a/src/tagstudio/qt/views/splash.py +++ b/src/tagstudio/qt/views/splash.py @@ -13,6 +13,7 @@ from PySide6.QtWidgets import QSplashScreen, QWidget from tagstudio.core.constants import VERSION, VERSION_BRANCH from tagstudio.qt.global_settings import Splash from tagstudio.qt.resource_manager import ResourceManager +from tagstudio.qt.translations import Translations logger = structlog.get_logger(__name__) @@ -20,12 +21,11 @@ logger = structlog.get_logger(__name__) class SplashScreen: """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 ''}" - ) - DEFAULT_SPLASH = Splash.GOO_GEARS + COPYRIGHT_YEARS: str = "2021-2026" + COPYRIGHT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien & TagStudio Contributors" + COPYRIGHT_COMPACT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien\n& TagStudio Contributors" + VERSION_STR: str = f"{Translations['about.version']} {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" # noqa: E501 + DEFAULT_SPLASH = Splash.AURORA def __init__( self, @@ -50,7 +50,8 @@ class SplashScreen: def get_pixmap(self) -> QPixmap: """Get the pixmap used for the splash screen.""" - pixmap: QPixmap | None = self.rm.get(f"splash_{self.splash_name}") # pyright: ignore[reportAssignmentType] + pixmap = self.rm.get(f"splash_{self.splash_name}") + assert isinstance(pixmap, QPixmap) if not pixmap: logger.error("[Splash] Splash screen not found:", splash_name=self.splash_name) pixmap = QPixmap(960, 540) @@ -70,19 +71,19 @@ class SplashScreen: 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), - SplashScreen.COPYRIGHT_STR, - ) - # Version pen = QPen(QColor("#809782ff")) painter.setPen(pen) painter.drawText( QRect(0, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), + SplashScreen.COPYRIGHT, + ) + # Version + pen = QPen(QColor("#9782ff")) + painter.setPen(pen) + painter.drawText( + QRect(0, -50, 960, 540), + int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), SplashScreen.VERSION_STR, ) @@ -91,20 +92,20 @@ class SplashScreen: font = painter.font() font.setPointSize(math.floor(22 * point_size_scale)) painter.setFont(font) - pen = QPen(QColor("#9782ff")) + pen = QPen(QColor("#809782ff")) painter.setPen(pen) painter.drawText( QRect(40, 450, 960, 540), - SplashScreen.COPYRIGHT_STR, + SplashScreen.COPYRIGHT_COMPACT, ) # Version font = painter.font() font.setPointSize(math.floor(22 * point_size_scale)) painter.setFont(font) - pen = QPen(QColor("#809782ff")) + pen = QPen(QColor("#9782ff")) painter.setPen(pen) painter.drawText( - QRect(40, 475, 960, 540), + QRect(40, 420, 960, 540), SplashScreen.VERSION_STR, ) @@ -121,7 +122,7 @@ class SplashScreen: painter.drawText( QRect(88, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft), - SplashScreen.COPYRIGHT_STR, + SplashScreen.COPYRIGHT, ) # Version font.setPointSize(math.floor(22 * point_size_scale)) @@ -134,17 +135,33 @@ class SplashScreen: SplashScreen.VERSION_STR, ) + case Splash.AURORA: + # Copyright + font = painter.font() + font.setPointSize(math.floor(22 * point_size_scale)) + painter.setFont(font) + pen = QPen(QColor("#907758FF")) + painter.setPen(pen) + painter.drawText( + QRect(0, -25, 960, 540), + int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), + SplashScreen.COPYRIGHT, + ) + # Version + pen = QPen(QColor("#7758FF")) + painter.setPen(pen) + painter.drawText( + QRect(0, -50, 960, 540), + int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), + SplashScreen.VERSION_STR, + ) + case _: pass pixmap.setDevicePixelRatio(self.ratio) pixmap = pixmap.scaledToWidth( - math.floor( - min( - (self.screen_width * self.ratio) / 4, - pixmap.width(), - ) - ), + math.floor(min((self.screen_width * self.ratio) / 4, pixmap.width())), # pyright: ignore[reportCallIssue] Qt.TransformationMode.SmoothTransformation, ) diff --git a/src/tagstudio/resources/icon.icns b/src/tagstudio/resources/icon.icns index 2c84be07..100f5309 100644 Binary files a/src/tagstudio/resources/icon.icns and b/src/tagstudio/resources/icon.icns differ diff --git a/src/tagstudio/resources/icon.ico b/src/tagstudio/resources/icon.ico index 71335bd5..e011c3ac 100644 Binary files a/src/tagstudio/resources/icon.ico and b/src/tagstudio/resources/icon.ico differ diff --git a/src/tagstudio/resources/icon.png b/src/tagstudio/resources/icon.png index dd3d2772..be9e7a82 100644 Binary files a/src/tagstudio/resources/icon.png and b/src/tagstudio/resources/icon.png differ diff --git a/src/tagstudio/resources/qt/images/box_icon_empty_128 - Copy.png b/src/tagstudio/resources/qt/images/box_icon_empty_128 - Copy.png deleted file mode 100644 index 4fc14bc6..00000000 Binary files a/src/tagstudio/resources/qt/images/box_icon_empty_128 - Copy.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/box_icon_filled_128 - Copy.png b/src/tagstudio/resources/qt/images/box_icon_filled_128 - Copy.png deleted file mode 100644 index 67dd6d5b..00000000 Binary files a/src/tagstudio/resources/qt/images/box_icon_filled_128 - Copy.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/bxs-clipboard-regular.png b/src/tagstudio/resources/qt/images/bxs-clipboard-regular.png new file mode 100644 index 00000000..1166820a Binary files /dev/null and b/src/tagstudio/resources/qt/images/bxs-clipboard-regular.png differ diff --git a/src/tagstudio/resources/qt/images/bxs-pencil-solid.png b/src/tagstudio/resources/qt/images/bxs-pencil-solid.png new file mode 100644 index 00000000..9841f739 Binary files /dev/null and b/src/tagstudio/resources/qt/images/bxs-pencil-solid.png differ diff --git a/src/tagstudio/resources/qt/images/bxs-trash-solid.png b/src/tagstudio/resources/qt/images/bxs-trash-solid.png new file mode 100644 index 00000000..f1c0c078 Binary files /dev/null and b/src/tagstudio/resources/qt/images/bxs-trash-solid.png differ diff --git a/src/tagstudio/resources/qt/images/bxs-volume-full-solid.png b/src/tagstudio/resources/qt/images/bxs-volume-full-solid.png new file mode 100644 index 00000000..f24ede9e Binary files /dev/null and b/src/tagstudio/resources/qt/images/bxs-volume-full-solid.png differ diff --git a/src/tagstudio/resources/qt/images/bxs-volume-mute-solid.png b/src/tagstudio/resources/qt/images/bxs-volume-mute-solid.png new file mode 100644 index 00000000..1e8bca80 Binary files /dev/null and b/src/tagstudio/resources/qt/images/bxs-volume-mute-solid.png differ diff --git a/src/tagstudio/resources/qt/images/clipboard_icon_128.png b/src/tagstudio/resources/qt/images/clipboard_icon_128.png deleted file mode 100644 index 9261b476..00000000 Binary files a/src/tagstudio/resources/qt/images/clipboard_icon_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/edit_icon_128.png b/src/tagstudio/resources/qt/images/edit_icon_128.png deleted file mode 100644 index 85f85d33..00000000 Binary files a/src/tagstudio/resources/qt/images/edit_icon_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/edit_icon_dark_128.png b/src/tagstudio/resources/qt/images/edit_icon_dark_128.png deleted file mode 100644 index 249da512..00000000 Binary files a/src/tagstudio/resources/qt/images/edit_icon_dark_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/pause.png b/src/tagstudio/resources/qt/images/pause.png new file mode 100644 index 00000000..7e70585e Binary files /dev/null and b/src/tagstudio/resources/qt/images/pause.png differ diff --git a/src/tagstudio/resources/qt/images/pause.svg b/src/tagstudio/resources/qt/images/pause.svg deleted file mode 100644 index f7777470..00000000 --- a/src/tagstudio/resources/qt/images/pause.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/tagstudio/resources/qt/images/play.svg b/src/tagstudio/resources/qt/images/play.svg deleted file mode 100644 index 3d5f6506..00000000 --- a/src/tagstudio/resources/qt/images/play.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/tagstudio/resources/qt/images/splash/aurora.png b/src/tagstudio/resources/qt/images/splash/aurora.png new file mode 100644 index 00000000..c1902c99 Binary files /dev/null and b/src/tagstudio/resources/qt/images/splash/aurora.png differ diff --git a/src/tagstudio/resources/qt/images/splitter_handle_128.png b/src/tagstudio/resources/qt/images/splitter_handle_128.png deleted file mode 100644 index 328a0dd5..00000000 Binary files a/src/tagstudio/resources/qt/images/splitter_handle_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/star_icon_empty_128 - Copy.png b/src/tagstudio/resources/qt/images/star_icon_empty_128 - Copy.png deleted file mode 100644 index ab1ef1ff..00000000 Binary files a/src/tagstudio/resources/qt/images/star_icon_empty_128 - Copy.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/star_icon_filled_128 - Copy.png b/src/tagstudio/resources/qt/images/star_icon_filled_128 - Copy.png deleted file mode 100644 index 0bfcf2b1..00000000 Binary files a/src/tagstudio/resources/qt/images/star_icon_filled_128 - Copy.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/tag_group_icon_128.png b/src/tagstudio/resources/qt/images/tag_group_icon_128.png deleted file mode 100644 index 18510d5d..00000000 Binary files a/src/tagstudio/resources/qt/images/tag_group_icon_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/tag_group_icon_dark_128.png b/src/tagstudio/resources/qt/images/tag_group_icon_dark_128.png deleted file mode 100644 index a698c030..00000000 Binary files a/src/tagstudio/resources/qt/images/tag_group_icon_dark_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/tagstudio_logo-text_color.png b/src/tagstudio/resources/qt/images/tagstudio_logo-text_color.png new file mode 100644 index 00000000..440b5837 Binary files /dev/null and b/src/tagstudio/resources/qt/images/tagstudio_logo-text_color.png differ diff --git a/src/tagstudio/resources/qt/images/tagstudio_logo-text_mono.png b/src/tagstudio/resources/qt/images/tagstudio_logo-text_mono.png new file mode 100644 index 00000000..a464e7a7 Binary files /dev/null and b/src/tagstudio/resources/qt/images/tagstudio_logo-text_mono.png differ diff --git a/src/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png b/src/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png deleted file mode 100644 index a5d33347..00000000 Binary files a/src/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/trash_icon_128.png b/src/tagstudio/resources/qt/images/trash_icon_128.png deleted file mode 100644 index a1d11843..00000000 Binary files a/src/tagstudio/resources/qt/images/trash_icon_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/trash_icon_dark_128.png b/src/tagstudio/resources/qt/images/trash_icon_dark_128.png deleted file mode 100644 index 775b55f1..00000000 Binary files a/src/tagstudio/resources/qt/images/trash_icon_dark_128.png and /dev/null differ diff --git a/src/tagstudio/resources/qt/images/volume.svg b/src/tagstudio/resources/qt/images/volume.svg deleted file mode 100644 index d240d7ae..00000000 --- a/src/tagstudio/resources/qt/images/volume.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/tagstudio/resources/qt/images/volume_mute.svg b/src/tagstudio/resources/qt/images/volume_mute.svg deleted file mode 100644 index 5ce8f0a4..00000000 --- a/src/tagstudio/resources/qt/images/volume_mute.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 1035c151..70aab24c 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -5,10 +5,12 @@ "about.license": "License", "about.module.found": "Found", "about.title": "About TagStudio", + "about.version": "Version", "about.website": "Website", "app.git": "Git Commit", "app.pre_release": "Pre-Release", "app.title": "{base_title} - Library '{library_dir}'", + "color_manager.title": "Manage Tag Colors", "color.color_border": "Use Secondary Color for Border", "color.confirm_delete": "Are you sure you want to delete the color \"{color_name}\"?", "color.delete": "Delete Tag", @@ -22,7 +24,6 @@ "color.primary_required": "Primary Color (Required)", "color.secondary": "Secondary Color", "color.title.no_color": "No Color", - "color_manager.title": "Manage Tag Colors", "dependency.missing.title": "{dependency} Not Found", "drop_import.description": "The following files match file paths that already exist in the library", "drop_import.duplicates_choice.plural": "The following {count} files match file paths that already exist in the library.", @@ -72,6 +73,13 @@ "entries.unlinked.unlinked_count": "Unlinked Entries: {count}", "ffmpeg.missing.description": "FFmpeg and/or FFprobe were not found. FFmpeg is required for multimedia playback and thumbnails.", "ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}
{ffprobe}: {ffprobe_status}", + "field_template_manager.title": "Library Field Templates", + "field_template.all_field_templates": "All Field Templates", + "field_template.create": "Create Field Template", + "field_template.create_add": "Create && Add \"{query}\"", + "field_type.datetime": "Datetime", + "field_type.text": "Text", + "field_type.unknown": "Unknown Type", "field.add": "Add Field", "field.add.plural": "Add Fields", "field.confirm_remove": "Are you sure you want to remove this \"{name}\" field?", @@ -80,13 +88,6 @@ "field.mixed_data": "Mixed Data", "field.paste": "Paste Field", "field.remove": "Remove Field", - "field_template.all_field_templates": "All Field Templates", - "field_template.create": "Create Field Template", - "field_template.create_add": "Create && Add \"{query}\"", - "field_template_manager.title": "Library Field Templates", - "field_type.datetime": "Datetime", - "field_type.text": "Text", - "field_type.unknown": "Unknown Type", "file.date_added": "Date Added", "file.date_created": "Date Created", "file.date_modified": "Date Modified", @@ -100,8 +101,8 @@ "file.duplicates.fix": "Fix Duplicate Files", "file.duplicates.matches": "Duplicate File Matches: {count}", "file.duplicates.matches_uninitialized": "Duplicate File Matches: N/A", - "file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.", "file.duplicates.mirror_entries": "&Mirror Entries", + "file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.", "file.duration": "Length", "file.not_found": "File Not Found", "file.open_file": "Open file", @@ -150,11 +151,11 @@ "generic.skip_alt": "&Skip", "generic.yes": "Yes", "home.search": "Search", - "home.search.view_limit": "View Limit:", "home.search_entries": "Search Entries", "home.search_field_templates": "Search Field Templates", "home.search_library": "Search Library", "home.search_tags": "Search Tags", + "home.search.view_limit": "View Limit:", "home.show_hidden_entries": "Show Hidden Entries", "home.thumbnail_size": "Thumbnail Size", "home.thumbnail_size.extra_large": "Extra Large Thumbnails", @@ -187,13 +188,6 @@ "json_migration.title.new_lib": "

v9.5+ Library

", "json_migration.title.old_lib": "

v9.4 Library

", "landing.open_create_library": "Open/Create Library {shortcut}", - "library.missing": "Library Location is Missing", - "library.name": "Library", - "library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found", - "library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found", - "library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...", - "library.refresh.title": "Refreshing Directories", - "library.scan_library.title": "Scanning Library", "library_info.cleanup": "Cleanup", "library_info.cleanup.backups": "Library Backups:", "library_info.cleanup.dupe_files": "Duplicate Files:", @@ -213,6 +207,13 @@ "library_object.name_required": "Name (Required)", "library_object.slug": "ID Slug", "library_object.slug_required": "ID Slug (Required)", + "library.missing": "Library Location is Missing", + "library.name": "Library", + "library.refresh.scanning_preparing": "Scanning Directories for New Files...\nPreparing...", + "library.refresh.scanning.plural": "Scanning Directories for New Files...\n{searched_count} Files Searched, {found_count} New Files Found", + "library.refresh.scanning.singular": "Scanning Directories for New Files...\n{searched_count} File Searched, {found_count} New Files Found", + "library.refresh.title": "Refreshing Directories", + "library.scan_library.title": "Scanning Library", "macros.running.dialog.new_entries": "Running Configured Macros on {count}/{total} New File Entries...", "macros.running.dialog.title": "Running Macros on New Entries", "media_player.autoplay": "Autoplay", @@ -288,6 +289,7 @@ "settings.show_filenames_in_grid": "Show Filenames in Grid", "settings.show_recent_libraries": "Show Recent Libraries", "settings.splash.label": "Splash Screen", + "settings.splash.option.aurora": "Aurora (9.6)", "settings.splash.option.classic": "Classic (9.0)", "settings.splash.option.default": "Default", "settings.splash.option.goo_gears": "Open Source (9.4)", @@ -323,11 +325,12 @@ "status.library_version_found": "Found:", "status.library_version_mismatch": "Library Version Mismatch!", "status.results": "Results", - "status.results.invalid_syntax": "Invalid Search Syntax:", "status.results_found": "{count} Results Found ({time_span})", + "status.results.invalid_syntax": "Invalid Search Syntax:", + "tag_manager.title": "Library Tags", "tag.add": "Add Tag", - "tag.add.plural": "Add Tags", "tag.add_to_search": "Add to Search", + "tag.add.plural": "Add Tags", "tag.aliases": "Aliases", "tag.all_tags": "All Tags", "tag.choose_color": "Choose Tag Color", @@ -348,7 +351,6 @@ "tag.search_for_tag": "Search for Tag", "tag.shorthand": "Shorthand", "tag.tag_name_required": "Tag Name (Required)", - "tag_manager.title": "Library Tags", "trash.context.ambiguous": "Move file(s) to {trash_term}", "trash.context.plural": "Move files to {trash_term}", "trash.context.singular": "Move file to {trash_term}", diff --git a/tests/qt/test_resource_manager.py b/tests/qt/test_resource_manager.py index 5a6d72df..dd6afbb4 100644 --- a/tests/qt/test_resource_manager.py +++ b/tests/qt/test_resource_manager.py @@ -13,6 +13,6 @@ logger = structlog.get_logger() def test_get(): rm = ResourceManager() - for res in rm._map: # pyright: ignore[reportPrivateUsage] + for res in rm._map: assert rm.get(res), f"Could not get resource '{res}'" assert unwrap(rm.get_path(res)).exists(), f"Filepath for resource '{res}' does not exist"