Skip to content

feat: about section #712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 22, 2025
3 changes: 3 additions & 0 deletions tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
79 changes: 79 additions & 0 deletions tagstudio/src/qt/modals/about.py
Original file line number Diff line number Diff line change
@@ -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)
84 changes: 84 additions & 0 deletions tagstudio/src/qt/modals/ffmpeg_checker.py
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 4 additions & 0 deletions tagstudio/src/qt/resources.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"logo": {
"path": "icon.png",
"mode": "pil"
},
"play_icon": {
"path": "qt/images/play.svg",
"mode": "rb"
Expand Down
26 changes: 19 additions & 7 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import re
import sys
import time
import webbrowser
from pathlib import Path
from queue import Queue

Expand Down Expand Up @@ -75,8 +74,10 @@
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
Expand Down Expand Up @@ -160,12 +161,14 @@ def __init__(self, backend, args):

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,
Expand All @@ -177,6 +180,7 @@ def __init__(self, backend, args):
"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."""
Expand Down Expand Up @@ -475,12 +479,15 @@ def create_folders_tags_modal():
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)
Expand Down Expand Up @@ -537,6 +544,11 @@ def create_folders_tags_modal():
)
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()

Expand Down
Loading