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)