feat(ui): add about section (#712)

* added about section to the help menu and modal to display information

* load image through resource manager

* cleaned up about text

* remove visit github repo menu, moved to about modal

* added function to check ffmpeg version

* fixed version formatting

* copied in ffmpeg checker code from v9.4 with a warning message on startup if ffmpeg is not detected

* mypy fixes

* about.content spacing

* about.content title formatting

* added config path to about screen

* ruff fixes

---------

Co-authored-by: Sean Krueger <skrueger2270@gmail.com>
This commit is contained in:
mashed5894
2025-01-23 01:48:57 +02:00
committed by GitHub
parent ea9b57b85f
commit 778028923e
5 changed files with 189 additions and 7 deletions

View File

@@ -66,6 +66,8 @@
"folders_to_tags.description": "Creates tags based on your folder structure and applies them to your entries.\n The structure below shows all the tags that will be created and what entries they will be applied to.",
"folders_to_tags.open_all": "Open All",
"folders_to_tags.title": "Create Tags From Folders",
"about.title": "About",
"about.content": "<h2>TagStudio Alpha {version} ({branch})</h2><p>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.</p>License: GPLv3<br>Config path: {config_path}<br>FFmpeg: {ffmpeg}<br>FFprobe: {ffprobe}<p><a href=\"https://github.com/TagStudioDev/TagStudio\">GitHub</a> | <a href=\"https://docs.tagstud.io\">Documentation</a> | <a href=\"https://discord.com/invite/hRNnVKhF2G\">Discord</a></p>",
"generic.add": "Add",
"generic.apply_alt": "&Apply",
"generic.apply": "Apply",
@@ -166,6 +168,7 @@
"menu.file.save_library": "Save Library",
"menu.file": "&File",
"menu.help": "&Help",
"menu.help.about": "About",
"menu.macros.folders_to_tags": "Folders to Tags",
"menu.macros": "&Macros",
"menu.select": "Select",

View File

@@ -0,0 +1,79 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PIL import ImageQt
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.core.constants import VERSION, VERSION_BRANCH
from src.qt.modals.ffmpeg_checker import FfmpegChecker
from src.qt.resource_manager import ResourceManager
from src.qt.translations import Translations
class AboutModal(QWidget):
def __init__(self, config_path):
super().__init__()
Translations.translate_with_setter(self.setWindowTitle, "about.title")
self.fc: FfmpegChecker = FfmpegChecker()
self.rm: ResourceManager = ResourceManager()
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 500)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(20, 20, 20, 6)
self.logo_widget = QLabel()
self.logo_widget.setObjectName("logo")
self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.get("logo")))
self.logo_pixmap = self.logo_pixmap.scaledToWidth(
128, Qt.TransformationMode.SmoothTransformation
)
self.logo_widget.setPixmap(self.logo_pixmap)
self.logo_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.logo_widget.setContentsMargins(0, 0, 0, 20)
self.content_widget = QLabel()
self.content_widget.setObjectName("contentLabel")
self.content_widget.setWordWrap(True)
ff_version = self.fc.version()
ffmpeg = '<span style="color:red">Missing</span>'
if ff_version["ffmpeg"] is not None:
ffmpeg = '<span style="color:green">Found</span> (' + ff_version["ffmpeg"] + ")"
ffprobe = '<span style="color:red">Missing</span>'
if ff_version["ffprobe"] is not None:
ffprobe = '<span style="color:green">Found</span> (' + ff_version["ffprobe"] + ")"
Translations.translate_qobject(
self.content_widget,
"about.content",
version=VERSION,
branch=VERSION_BRANCH,
config_path=config_path,
ffmpeg=ffmpeg,
ffprobe=ffprobe,
)
self.content_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.button_widget = QWidget()
self.button_layout = QHBoxLayout(self.button_widget)
self.button_layout.addStretch(1)
self.close_button = QPushButton()
Translations.translate_qobject(self.close_button, "generic.close")
self.close_button.clicked.connect(lambda: self.close())
self.close_button.setMaximumWidth(80)
self.button_layout.addWidget(self.close_button)
self.root_layout.addWidget(self.logo_widget)
self.root_layout.addWidget(self.content_widget, Qt.AlignmentFlag.AlignTop)
self.root_layout.addWidget(self.button_widget)

View File

@@ -0,0 +1,84 @@
import contextlib
import subprocess
from shutil import which
import structlog
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QMessageBox
from src.qt.helpers.vendored.ffmpeg import FFPROBE_CMD
logger = structlog.get_logger(__name__)
class FfmpegChecker(QMessageBox):
"""A warning dialog for if FFmpeg is missing."""
HELP_URL = "https://docs.tagstud.io/help/ffmpeg/"
def __init__(self):
super().__init__()
self.setWindowTitle("Warning: Missing dependency")
self.setText("Warning: Could not find FFmpeg installation")
self.setIcon(QMessageBox.Icon.Warning)
# Blocks other application interactions until resolved
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setStandardButtons(
QMessageBox.StandardButton.Help
| QMessageBox.StandardButton.Ignore
| QMessageBox.StandardButton.Cancel
)
self.setDefaultButton(QMessageBox.StandardButton.Ignore)
# Enables the cancel button but hides it to allow for click X to close dialog
self.button(QMessageBox.StandardButton.Cancel).hide()
self.ffmpeg = False
self.ffprobe = False
def installed(self):
"""Checks if both FFmpeg and FFprobe are installed and in the PATH."""
if which("ffmpeg"):
self.ffmpeg = True
if which(FFPROBE_CMD):
self.ffprobe = True
logger.info("FFmpeg found: {self.ffmpeg}, FFprobe found: {self.ffprobe}")
return self.ffmpeg and self.ffprobe
def version(self):
"""Checks the version of ffprobe and ffmpeg and returns None if they dont exist."""
version: dict[str, str | None] = {"ffprobe": None, "ffmpeg": None}
self.installed()
if self.ffprobe:
ret = subprocess.run(
[FFPROBE_CMD, "-show_program_version"], shell=False, capture_output=True, text=True
)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffprobe"] = ret.stdout.split("\n")[1].replace("-", "=").split("=")[1]
if self.ffmpeg:
ret = subprocess.run(
["ffmpeg", "-version"], shell=False, capture_output=True, text=True
)
if ret.returncode == 0:
with contextlib.suppress(Exception):
version["ffmpeg"] = ret.stdout.replace("-", " ").split(" ")[2]
return version
def show_warning(self):
"""Displays the warning to the user and awaits respone."""
missing = "FFmpeg"
# If ffmpeg is installed but not ffprobe
if not self.ffprobe and self.ffmpeg:
missing = "FFprobe"
self.setText(f"Warning: Could not find {missing} installation")
self.setInformativeText(f"{missing} is required for multimedia thumbnails and playback")
# Shows the dialog
selection = self.exec()
# Selection will either be QMessageBox.Help or (QMessageBox.Ignore | QMessageBox.Cancel)
if selection == QMessageBox.StandardButton.Help:
QDesktopServices.openUrl(QUrl(self.HELP_URL))

View File

@@ -1,4 +1,8 @@
{
"logo": {
"path": "icon.png",
"mode": "pil"
},
"play_icon": {
"path": "qt/images/play.svg",
"mode": "rb"

View File

@@ -14,7 +14,6 @@ import os
import re
import sys
import time
import webbrowser
from pathlib import Path
from queue import Queue
@@ -75,8 +74,10 @@ from src.qt.flowlayout import FlowLayout
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.main_window import Ui_MainWindow
from src.qt.modals.about import AboutModal
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.modals.drop_import import DropImportModal
from src.qt.modals.ffmpeg_checker import FfmpegChecker
from src.qt.modals.file_extension import FileExtensionModal
from src.qt.modals.fix_dupes import FixDupeFilesModal
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
@@ -160,12 +161,14 @@ class QtDriver(DriverMixin, QObject):
self.SIGTERM.connect(self.handle_sigterm)
self.config_path = ""
if self.args.config_file:
path = Path(self.args.config_file)
if not path.exists():
logger.warning("Config File does not exist creating", path=path)
logger.info("Using Config File", path=path)
self.settings = QSettings(str(path), QSettings.Format.IniFormat)
self.config_path = str(path)
else:
self.settings = QSettings(
QSettings.Format.IniFormat,
@@ -177,6 +180,7 @@ class QtDriver(DriverMixin, QObject):
"Config File not specified, using default one",
filename=self.settings.fileName(),
)
self.config_path = self.settings.fileName()
def init_workers(self):
"""Init workers for rendering thumbnails."""
@@ -475,12 +479,15 @@ class QtDriver(DriverMixin, QObject):
macros_menu.addAction(folders_to_tags_action)
# Help Menu ============================================================
self.repo_action = QAction(menu_bar)
Translations.translate_qobject(self.repo_action, "help.visit_github")
self.repo_action.triggered.connect(
lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio")
)
help_menu.addAction(self.repo_action)
def create_about_modal():
if not hasattr(self, "about_modal"):
self.about_modal = AboutModal(self.config_path)
self.about_modal.show()
self.about_action = QAction(menu_bar)
Translations.translate_qobject(self.about_action, "menu.help.about")
self.about_action.triggered.connect(create_about_modal)
help_menu.addAction(self.about_action)
self.set_macro_menu_viability()
menu_bar.addMenu(file_menu)
@@ -537,6 +544,11 @@ class QtDriver(DriverMixin, QObject):
)
self.open_library(path_result.library_path)
# check ffmpeg and show warning if not
self.ffmpeg_checker = FfmpegChecker()
if not self.ffmpeg_checker.installed():
self.ffmpeg_checker.show_warning()
app.exec()
self.shutdown()