diff --git a/src/tagstudio/core/constants.py b/src/tagstudio/core/constants.py index 4cd6f582..d0e4d6f3 100644 --- a/src/tagstudio/core/constants.py +++ b/src/tagstudio/core/constants.py @@ -4,6 +4,10 @@ VERSION: str = "9.5.7" # Major.Minor.Patch VERSION_BRANCH: str = "" # Usually "" or "Pre-Release" +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" + GITHUB_RELEASE_URL = "https://github.com/TagStudioDev/TagStudio/releases/latest" # The folder & file names where TagStudio keeps its data relative to a library. diff --git a/src/tagstudio/qt/mixed/about_modal.py b/src/tagstudio/qt/mixed/about_modal.py index 059ce54e..7429d35f 100644 --- a/src/tagstudio/qt/mixed/about_modal.py +++ b/src/tagstudio/qt/mixed/about_modal.py @@ -4,10 +4,11 @@ import math from pathlib import Path +from shutil import which from PIL import ImageQt -from PySide6.QtCore import Qt -from PySide6.QtGui import QGuiApplication, QPixmap +from PySide6.QtCore import QSize, Qt +from PySide6.QtGui import QGuiApplication, QPalette, QPixmap from PySide6.QtWidgets import ( QFormLayout, QHBoxLayout, @@ -18,7 +19,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from tagstudio.core.constants import VERSION, VERSION_BRANCH +from tagstudio.core.constants import COPYRIGHT, VERSION, VERSION_BRANCH from tagstudio.core.enums import Theme from tagstudio.core.ts_core import TagStudioCore from tagstudio.core.utils.types import unwrap @@ -26,9 +27,15 @@ from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color from tagstudio.qt.previews.vendored import ffmpeg from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations +from tagstudio.qt.utils.file_opener import open_file +from tagstudio.qt.views.clickable_label import ClickableLabel class AboutModal(QWidget): + """Modal window showing information about the TagStudio application.""" + + VERSION_STR: str = f"{Translations['about.version']} {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" # noqa: E501 + def __init__(self, config_path: Path | str): super().__init__() self.setWindowTitle(Translations["about.title"]) @@ -46,12 +53,14 @@ class AboutModal(QWidget): "font-weight: 500;" "padding: 2px;" ) + self.setStyleSheet("QLabel {color: white}") self.setWindowModality(Qt.WindowModality.ApplicationModal) - self.setMinimumSize(420, 500) - self.setMaximumSize(600, 800) + self.setFixedWidth(600) + self.setMinimumHeight(600) + self.setMaximumHeight(900) self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(0, 12, 0, 0) + self.root_layout.setContentsMargins(0, 100, 0, 0) self.root_layout.setSpacing(0) self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignCenter) @@ -71,13 +80,19 @@ class AboutModal(QWidget): self.logo_widget.setContentsMargins(0, 0, 0, 0) self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) - # Title ---------------------------------------------------------------- - branch: str = (" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else "" - self.title_label = QLabel(f"

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

") - self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + # Version -------------------------------------------------------------- + self.version_label = QLabel(f"

{AboutModal.VERSION_STR}

") + self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + # self.version_label.setStyleSheet("QLabel {color: #9782ff}") + + # Copyright ------------------------------------------------------------ + self.copyright_label = QLabel(COPYRIGHT) + self.copyright_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.copyright_label.setStyleSheet("QLabel {color: #809782ff}") # Description ---------------------------------------------------------- self.desc_label = QLabel(Translations["about.description"]) + self.desc_label.setMaximumWidth(500) self.desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.desc_label.setWordWrap(True) self.desc_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) @@ -86,6 +101,7 @@ class AboutModal(QWidget): ff_version = ffmpeg.version() red = get_ui_color(ColorType.PRIMARY, UiColor.RED) green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN) + amber = get_ui_color(ColorType.PRIMARY, UiColor.AMBER) missing = Translations["generic.missing"] found = Translations["about.module.found"] @@ -101,6 +117,10 @@ class AboutModal(QWidget): f'{found} (' + ff_version["ffprobe"] + ")" ) + ripgrep_status = f'{missing}' + if which("rg") is not None: + ripgrep_status = f'{found}' + self.system_info_widget = QWidget() self.system_info_layout = QFormLayout(self.system_info_widget) self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight) @@ -118,34 +138,61 @@ class AboutModal(QWidget): version_content.setMaximumWidth(version_content.sizeHint().width()) self.system_info_layout.addRow(version_title, version_content) - # License - license_title = QLabel(f"{Translations['about.license']}") - license_content = QLabel("GPLv3") - license_content.setStyleSheet(self.form_content_style) - license_content.setMaximumWidth(license_content.sizeHint().width()) - self.system_info_layout.addRow(license_title, license_content) - # Config Path config_path_title = QLabel(f"{Translations['about.config_path']}") - config_path_content = QLabel(f"{config_path}") + config_path_content = ClickableLabel() + config_path_content.setText(f"{config_path}") # TODO: Pass in constructor after #1386 + config_path_content.clicked.connect(lambda: open_file(config_path, file_manager=True)) + config_path_content.setCursor(Qt.CursorShape.PointingHandCursor) config_path_content.setStyleSheet(self.form_content_style) config_path_content.setWordWrap(True) self.system_info_layout.addRow(config_path_title, config_path_content) + # TODO: Add row for "App Cache Path" (currently that TagStudio.ini file) + # FFmpeg Status ffmpeg_path_title = QLabel("FFmpeg") - ffmpeg_path_content = QLabel(f"{ffmpeg_status}") + ffmpeg_path_content = ClickableLabel() + ffmpeg_path_content.setText(f"{ffmpeg_status}") # TODO: Pass in constructor after #1386 + ffmpeg_location = which(ffmpeg._get_ffmpeg_location()) # pyright: ignore[reportPrivateUsage] + if ffmpeg_location: + ffmpeg_path_content.clicked.connect( + lambda: open_file(ffmpeg_location, file_manager=True) + ) + ffmpeg_path_content.setCursor(Qt.CursorShape.PointingHandCursor) ffmpeg_path_content.setStyleSheet(self.form_content_style) ffmpeg_path_content.setMaximumWidth(ffmpeg_path_content.sizeHint().width()) self.system_info_layout.addRow(ffmpeg_path_title, ffmpeg_path_content) # FFprobe Status ffprobe_path_title = QLabel("FFprobe") - ffprobe_path_content = QLabel(f"{ffprobe_status}") + ffprobe_path_content = ClickableLabel() + ffprobe_path_content.setText(f"{ffprobe_status}") # TODO: Pass in constructor after #1386 + ffprobe_location = which(ffmpeg._get_ffprobe_location()) # pyright: ignore[reportPrivateUsage] + if ffprobe_location: + ffprobe_path_content.clicked.connect( + lambda: open_file(ffprobe_location, file_manager=True) + ) + ffprobe_path_content.setCursor(Qt.CursorShape.PointingHandCursor) ffprobe_path_content.setStyleSheet(self.form_content_style) ffprobe_path_content.setMaximumWidth(ffprobe_path_content.sizeHint().width()) self.system_info_layout.addRow(ffprobe_path_title, ffprobe_path_content) + # ripgrep Status + # TODO: Add a central class to find ripgrep info, similar to ffmpeg + ripgrep_path_title = QLabel("ripgrep") # NOTE: Don't localize + ripgrep_path_content = ClickableLabel() + ripgrep_path_content.setText(f"{ripgrep_status}") # TODO: Pass in constructor after #1386 + ripgrep_location = which("rg") + if ripgrep_location: + ripgrep_path_content.clicked.connect( + lambda: open_file(ripgrep_location, file_manager=True) + ) + ripgrep_path_content.setCursor(Qt.CursorShape.PointingHandCursor) + ripgrep_path_content.setStyleSheet(self.form_content_style) + ripgrep_path_content.setMaximumWidth(ripgrep_path_content.sizeHint().width()) + self.system_info_layout.addRow(ripgrep_path_title, ripgrep_path_content) + # Links ---------------------------------------------------------------- repo_link = "https://github.com/TagStudioDev/TagStudio" docs_link = "https://docs.tagstud.io" @@ -156,6 +203,7 @@ class AboutModal(QWidget): f'{Translations["about.documentation"]} | ' f'Discord

' ) + self.links_label.setStyleSheet("QLabel {color: #809782ff}") self.links_label.setWordWrap(True) self.links_label.setOpenExternalLinks(True) self.links_label.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -172,12 +220,21 @@ class AboutModal(QWidget): # Add Widgets to Layouts ----------------------------------------------- self.content_layout.addWidget(self.logo_widget) - self.content_layout.addWidget(self.title_label) + self.content_layout.addWidget(self.version_label) self.content_layout.addWidget(self.desc_label) self.content_layout.addWidget(self.system_info_widget) self.content_layout.addStretch(1) self.content_layout.addWidget(self.links_label) + self.content_layout.addWidget(self.copyright_label) self.content_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.bg_image = self.rm.about_bg + self.bg_image = self.bg_image.scaled( + QSize(self.width(), self.maximumHeight()), Qt.AspectRatioMode.IgnoreAspectRatio + ) + palette = QPalette() + palette.setBrush(QPalette.ColorRole.Window, self.bg_image) + self.setPalette(palette) + self.root_layout.addWidget(self.content_widget) self.root_layout.addWidget(self.button_widget) diff --git a/src/tagstudio/qt/previews/vendored/ffmpeg.py b/src/tagstudio/qt/previews/vendored/ffmpeg.py index 84d6ba19..e3b3d57b 100644 --- a/src/tagstudio/qt/previews/vendored/ffmpeg.py +++ b/src/tagstudio/qt/previews/vendored/ffmpeg.py @@ -27,6 +27,7 @@ FFMPEG_MACOS_LOCATIONS: list[str] = [ ] +# TODO: Make this more intuitive to use in other classes def _get_ffprobe_location() -> str: cmd: str = "ffprobe" if platform.system() == "Darwin": @@ -40,6 +41,7 @@ def _get_ffprobe_location() -> str: return cmd +# TODO: Make this more intuitive to use in other classes def _get_ffmpeg_location() -> str: cmd: str = "ffmpeg" if platform.system() == "Darwin": diff --git a/src/tagstudio/qt/resource_manager.pyi b/src/tagstudio/qt/resource_manager.pyi index 04872140..be327a0f 100644 --- a/src/tagstudio/qt/resource_manager.pyi +++ b/src/tagstudio/qt/resource_manager.pyi @@ -18,6 +18,7 @@ class ResourceManager: _instance: ResourceManager | None # Resources IDs from "resources.json" + about_bg: QPixmap adobe_illustrator: Image.Image adobe_photoshop: Image.Image affinity_photo: Image.Image diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 5ec05203..48b6f8f3 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -1,4 +1,8 @@ { + "about_bg": { + "mode": "qpixmap", + "path": "qt/images/about_bg.jpg" + }, "adobe_illustrator": { "mode": "pil", "path": "qt/images/file_icons/adobe_illustrator.png" @@ -91,6 +95,14 @@ "mode": "pil", "path": "qt/images/file_icons/model.png" }, + "mute_icon": { + "mode": "pil", + "path": "qt/images/bxs-volume-mute-solid.png" + }, + "pause_icon": { + "mode": "pil", + "path": "qt/images/pause.png" + }, "presentation": { "mode": "pil", "path": "qt/images/file_icons/presentation.png" @@ -158,13 +170,5 @@ "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/views/splash.py b/src/tagstudio/qt/views/splash.py index a71e5bc8..8f63c78b 100644 --- a/src/tagstudio/qt/views/splash.py +++ b/src/tagstudio/qt/views/splash.py @@ -10,7 +10,7 @@ from PySide6.QtCore import QRect, Qt from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap from PySide6.QtWidgets import QSplashScreen, QWidget -from tagstudio.core.constants import VERSION, VERSION_BRANCH +from tagstudio.core.constants import COPYRIGHT, COPYRIGHT_COMPACT, VERSION, VERSION_BRANCH from tagstudio.qt.global_settings import Splash from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations @@ -21,9 +21,6 @@ logger = structlog.get_logger(__name__) class SplashScreen: """The custom splash screen widget for TagStudio.""" - 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 @@ -76,7 +73,7 @@ class SplashScreen: painter.drawText( QRect(0, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - SplashScreen.COPYRIGHT, + COPYRIGHT, ) # Version pen = QPen(QColor("#9782ff")) @@ -96,7 +93,7 @@ class SplashScreen: painter.setPen(pen) painter.drawText( QRect(40, 450, 960, 540), - SplashScreen.COPYRIGHT_COMPACT, + COPYRIGHT_COMPACT, ) # Version font = painter.font() @@ -122,7 +119,7 @@ class SplashScreen: painter.drawText( QRect(88, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft), - SplashScreen.COPYRIGHT, + COPYRIGHT, ) # Version font.setPointSize(math.floor(22 * point_size_scale)) @@ -145,7 +142,7 @@ class SplashScreen: painter.drawText( QRect(0, -25, 960, 540), int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - SplashScreen.COPYRIGHT, + COPYRIGHT, ) # Version pen = QPen(QColor("#7758FF")) diff --git a/src/tagstudio/resources/qt/images/about_bg.jpg b/src/tagstudio/resources/qt/images/about_bg.jpg new file mode 100644 index 00000000..8ff0a6d5 Binary files /dev/null and b/src/tagstudio/resources/qt/images/about_bg.jpg differ diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 70aab24c..80b27989 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -1,8 +1,8 @@ { "about.config_path": "Config Path", + "about.app_cache_path": "App Cache Path", "about.description": "TagStudio is a photo and 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.", "about.documentation": "Documentation", - "about.license": "License", "about.module.found": "Found", "about.title": "About TagStudio", "about.version": "Version", diff --git a/tests/test_translations.py b/tests/test_translations.py index 8d0533f3..5a699de5 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -3,6 +3,7 @@ import string +import warnings from pathlib import Path import pytest @@ -57,6 +58,8 @@ def test_format_key_validity(translation_filename: str): def test_for_unnecessary_translations(translation_filename: str): default_translation = load_translation("en.json") translation = load_translation(translation_filename) - assert set(default_translation.keys()).issuperset(translation.keys()), ( - f"Translation {translation_filename} has unnecessary keys ({set(translation.keys()).difference(default_translation.keys())})" # noqa: E501 - ) + if not set(default_translation.keys()).issuperset(translation.keys()): + message = str( + f"Translation {translation_filename} has unnecessary keys ({set(translation.keys()).difference(default_translation.keys())})", # noqa: E501 + ) + warnings.warn(message, stacklevel=1)