diff --git a/src/tagstudio/core/constants.py b/src/tagstudio/core/constants.py
index 7b323c8c..a746f038 100644
--- a/src/tagstudio/core/constants.py
+++ b/src/tagstudio/core/constants.py
@@ -11,6 +11,7 @@ COPYRIGHT_COMPACT: str = f"© {COPYRIGHT_YEARS} Travis Abendshien\n& TagStudio C
GITHUB_REPO_URL = "https://github.com/TagStudioDev/TagStudio"
GITHUB_RELEASE_URL = "https://github.com/TagStudioDev/TagStudio/releases/latest"
DOCS_URL = "https://docs.tagstud.io"
+FFMPEG_HELP_URL = "https://docs.tagstud.io/help/ffmpeg"
DISCORD_URL = "https://discord.com/invite/hRNnVKhF2G"
# The folder & file names where TagStudio keeps its data relative to a library.
diff --git a/src/tagstudio/core/driver.py b/src/tagstudio/core/driver.py
index cf50c45e..4d73a92a 100644
--- a/src/tagstudio/core/driver.py
+++ b/src/tagstudio/core/driver.py
@@ -8,7 +8,7 @@ import structlog
from PySide6.QtCore import QSettings
from tagstudio.core.constants import TS_FOLDER_NAME
-from tagstudio.core.enums import SettingItems
+from tagstudio.core.enums import AppCacheItems
from tagstudio.core.library.alchemy.library import LibraryStatus
from tagstudio.qt.global_settings import GlobalSettings
@@ -30,16 +30,16 @@ class DriverMixin:
logger.error("Path does not exist.", open_path=open_path)
return LibraryStatus(success=False, message="Path does not exist.")
elif self.settings.open_last_loaded_on_startup and self.cached_values.value(
- SettingItems.LAST_LIBRARY
+ AppCacheItems.LAST_LIBRARY
):
- library_path = Path(str(self.cached_values.value(SettingItems.LAST_LIBRARY)))
+ library_path = Path(str(self.cached_values.value(AppCacheItems.LAST_LIBRARY)))
if not (library_path / TS_FOLDER_NAME).exists():
logger.error(
"TagStudio folder does not exist.",
library_path=library_path,
ts_folder=TS_FOLDER_NAME,
)
- self.cached_values.setValue(SettingItems.LAST_LIBRARY, "")
+ self.cached_values.setValue(AppCacheItems.LAST_LIBRARY, "")
# dont consider this a fatal error, just skip opening the library
library_path = None
diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py
index c2d6a9c2..ae8bf8bc 100644
--- a/src/tagstudio/core/enums.py
+++ b/src/tagstudio/core/enums.py
@@ -5,11 +5,12 @@
import enum
-class SettingItems(enum.StrEnum):
+class AppCacheItems(enum.StrEnum):
"""List of setting item names."""
LAST_LIBRARY = "last_library"
LIBS_LIST = "libs_list"
+ DISMISSED_UPDATE = "dismissed_update"
class ShowFilepathOption(enum.IntEnum):
diff --git a/src/tagstudio/qt/controllers/ffmpeg_missing_message_box.py b/src/tagstudio/qt/controllers/ffmpeg_missing_message_box.py
deleted file mode 100644
index a3c71176..00000000
--- a/src/tagstudio/qt/controllers/ffmpeg_missing_message_box.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# SPDX-FileCopyrightText: (c) TagStudio Contributors
-# SPDX-License-Identifier: GPL-3.0-only
-
-
-from shutil import which
-
-import structlog
-from PySide6.QtCore import Qt, QUrl
-from PySide6.QtGui import QDesktopServices
-from PySide6.QtWidgets import QMessageBox
-
-from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
-from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
-from tagstudio.qt.translations import Translations
-
-logger = structlog.get_logger(__name__)
-
-
-class FfmpegMissingMessageBox(QMessageBox):
- """A warning dialog for if FFmpeg is missing."""
-
- HELP_URL = "https://docs.tagstud.io/help/ffmpeg/"
-
- def __init__(self):
- super().__init__()
-
- ffmpeg = "FFmpeg"
- ffprobe = "FFprobe"
- title = Translations.format("dependency.missing.title", dependency=ffmpeg)
- self.setWindowTitle(title)
- self.setIcon(QMessageBox.Icon.Warning)
- 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.button(QMessageBox.StandardButton.Help).clicked.connect(
- lambda: QDesktopServices.openUrl(QUrl(self.HELP_URL))
- )
-
- red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
- green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
- missing = f"{Translations['generic.missing']}"
- found = f"{Translations['about.module.found']}"
- status = Translations.format(
- "ffmpeg.missing.status",
- ffmpeg=ffmpeg,
- ffmpeg_status=found if which(FFMPEG_CMD) else missing,
- ffprobe=ffprobe,
- ffprobe_status=found if which(FFPROBE_CMD) else missing,
- )
- self.setText(f"{Translations['ffmpeg.missing.description']}
{status}")
diff --git a/src/tagstudio/qt/controllers/preview_panel_controller.py b/src/tagstudio/qt/controllers/preview_panel_controller.py
index d3852a80..8c276132 100644
--- a/src/tagstudio/qt/controllers/preview_panel_controller.py
+++ b/src/tagstudio/qt/controllers/preview_panel_controller.py
@@ -3,12 +3,14 @@
import typing
+from shutil import which
from warnings import catch_warnings
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
+from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
@@ -21,6 +23,7 @@ class PreviewPanel(PreviewPanelView):
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
+ self._thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
@typing.override
def _add_field_button_callback(self) -> None:
@@ -50,3 +53,10 @@ class PreviewPanel(PreviewPanelView):
self._containers.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._containers.update_from_entry(self._selected[0])
+
+ def _toggle_ffmpeg_warning(self, enable_warning: bool = True) -> None:
+ if enable_warning and (not which(FFMPEG_CMD) or not which(FFPROBE_CMD)):
+ self._ffmpeg_warning_widget.show()
+ return
+
+ self._ffmpeg_warning_widget.hide()
diff --git a/src/tagstudio/qt/controllers/out_of_date_message_box.py b/src/tagstudio/qt/controllers/update_available_message_box.py
similarity index 73%
rename from src/tagstudio/qt/controllers/out_of_date_message_box.py
rename to src/tagstudio/qt/controllers/update_available_message_box.py
index 71412f1e..a9415673 100644
--- a/src/tagstudio/qt/controllers/out_of_date_message_box.py
+++ b/src/tagstudio/qt/controllers/update_available_message_box.py
@@ -3,11 +3,12 @@
import math
+from functools import partial
import structlog
from PIL import ImageQt
from PySide6.QtCore import Qt
-from PySide6.QtGui import QPixmap
+from PySide6.QtGui import QDesktopServices, QPixmap
from PySide6.QtWidgets import QMessageBox
from tagstudio.core.constants import GITHUB_RELEASE_URL, VERSION
@@ -20,14 +21,13 @@ from tagstudio.qt.translations import Translations
logger = structlog.get_logger(__name__)
-class OutOfDateMessageBox(QMessageBox):
+class UpdateAvailableMessageBox(QMessageBox):
"""A warning dialog for if the TagStudio is not running under the latest release version."""
def __init__(self):
super().__init__()
rm = ResourceManager()
-
title = Translations["version_modal.title"]
self.setWindowTitle(title)
pixel_ratio = self.devicePixelRatio()
@@ -40,13 +40,19 @@ class OutOfDateMessageBox(QMessageBox):
icon.setDevicePixelRatio(pixel_ratio)
self.setIconPixmap(icon)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
+ self.setStyleSheet("QPushButton {padding: 3px 8px;}")
self.setStandardButtons(
- QMessageBox.StandardButton.Ignore | QMessageBox.StandardButton.Cancel
+ QMessageBox.StandardButton.Close
+ | QMessageBox.StandardButton.Ignore
+ | QMessageBox.StandardButton.Ok
)
- 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.setDefaultButton(QMessageBox.StandardButton.Ok)
+ self.button(QMessageBox.StandardButton.Ok).setText(Translations["update.view_update"])
+ self.button(QMessageBox.StandardButton.Ok).clicked.connect(
+ partial(QDesktopServices.openUrl, GITHUB_RELEASE_URL)
+ )
+ self.button(QMessageBox.StandardButton.Ignore).setText(Translations["generic.dont_remind"])
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py
index b7f6232d..ee087384 100644
--- a/src/tagstudio/qt/previews/renderer.py
+++ b/src/tagstudio/qt/previews/renderer.py
@@ -98,9 +98,9 @@ Image.MAX_IMAGE_PIXELS = None
register_heif_opener()
try:
- import pillow_jxl # noqa: F401 # pyright: ignore[reportUnusedImport]
-except ImportError:
- logger.exception('[ThumbRenderer] Could not import the "pillow_jxl" module')
+ import pillow_jxl # noqa: F401 # pyright: ignore
+except ImportError as e:
+ logger.error('[ThumbRenderer] Could not import the "pillow_jxl" module', error=e)
class _SevenZipFile(py7zr.SevenZipFile):
diff --git a/src/tagstudio/qt/resource_manager.pyi b/src/tagstudio/qt/resource_manager.pyi
index be327a0f..986ed884 100644
--- a/src/tagstudio/qt/resource_manager.pyi
+++ b/src/tagstudio/qt/resource_manager.pyi
@@ -22,6 +22,7 @@ class ResourceManager:
adobe_illustrator: Image.Image
adobe_photoshop: Image.Image
affinity_photo: Image.Image
+ alert: QPixmap
archive: Image.Image
audio: Image.Image
broken_link_icon: Image.Image
diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json
index 48b6f8f3..6533ce38 100644
--- a/src/tagstudio/qt/resources.json
+++ b/src/tagstudio/qt/resources.json
@@ -15,6 +15,10 @@
"mode": "pil",
"path": "qt/images/file_icons/affinity_photo.png"
},
+ "alert": {
+ "mode": "qpixmap",
+ "path": "qt/images/alert.png"
+ },
"archive": {
"mode": "pil",
"path": "qt/images/file_icons/archive.png"
diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py
index a5a9c840..50dda1af 100644
--- a/src/tagstudio/qt/ts_qt.py
+++ b/src/tagstudio/qt/ts_qt.py
@@ -17,9 +17,9 @@ import sys
import time
from argparse import Namespace
from collections import OrderedDict
+from functools import partial
from pathlib import Path
from queue import Queue
-from shutil import which
from typing import TypeVar
from warnings import catch_warnings
@@ -37,23 +37,14 @@ from PySide6.QtGui import (
QMouseEvent,
QPalette,
)
-from PySide6.QtWidgets import (
- QApplication,
- QFileDialog,
- QMessageBox,
- QPushButton,
- QScrollArea,
-)
+from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QPushButton, QScrollArea
# This import has side-effect of importing PySide resources
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
from tagstudio.core.driver import DriverMixin
-from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption
-from tagstudio.core.library.alchemy.enums import (
- BrowsingState,
- SortingModeEnum,
-)
+from tagstudio.core.enums import AppCacheItems, MacroID, ShowFilepathOption
+from tagstudio.core.library.alchemy.enums import BrowsingState, SortingModeEnum
from tagstudio.core.library.alchemy.library import Library, LibraryStatus
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.library.ignore import Ignore
@@ -64,18 +55,13 @@ from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
-from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchPanel
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
-from tagstudio.qt.controllers.out_of_date_message_box import OutOfDateMessageBox
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal, TagSearchPanel
-from tagstudio.qt.global_settings import (
- DEFAULT_GLOBAL_SETTINGS_PATH,
- GlobalSettings,
- Theme,
-)
+from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox
+from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
from tagstudio.qt.mixed.about_modal import AboutModal
from tagstudio.qt.mixed.build_tag import BuildTagPanel
from tagstudio.qt.mixed.drop_import_modal import DropImportModal
@@ -89,7 +75,6 @@ from tagstudio.qt.mixed.settings_panel import SettingsPanel
from tagstudio.qt.mixed.tag_color_manager import TagColorManager
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.platform_strings import trash_term
-from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
@@ -112,9 +97,9 @@ BADGE_TAGS = {
if sys.platform == "win32":
from signal import SIGINT, SIGTERM, signal
- SIGQUIT = SIGTERM
+ SIGQUIT = SIGTERM # pyright: ignore
else:
- from signal import SIGINT, SIGQUIT, SIGTERM, signal
+ from signal import SIGINT, SIGQUIT, SIGTERM, signal # pyright: ignore
logger = structlog.get_logger(__name__)
@@ -636,14 +621,7 @@ class QtDriver(DriverMixin, QObject):
if path_result.success and path_result.library_path:
self.open_library(path_result.library_path)
- # Check if FFmpeg or FFprobe are missing and show warning if so
- if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
- FfmpegMissingMessageBox().show()
-
- latest_version = TagStudioCore.get_most_recent_release_version()
- if latest_version and is_version_outdated(VERSION, latest_version):
- OutOfDateMessageBox().exec()
-
+ self.check_for_update()
self.app.exec()
self.shutdown()
@@ -783,7 +761,7 @@ class QtDriver(DriverMixin, QObject):
self.main_window.status_bar.showMessage(Translations["status.library_closing"])
start_time = time.time()
- self.cached_values.setValue(SettingItems.LAST_LIBRARY, str(self.lib.library_dir))
+ self.cached_values.setValue(AppCacheItems.LAST_LIBRARY, str(self.lib.library_dir))
self.cached_values.sync()
# Reset library state
@@ -1513,7 +1491,7 @@ class QtDriver(DriverMixin, QObject):
)
def remove_recent_library(self, item_key: str):
- self.cached_values.beginGroup(SettingItems.LIBS_LIST)
+ self.cached_values.beginGroup(AppCacheItems.LIBS_LIST)
self.cached_values.remove(item_key)
self.cached_values.endGroup()
self.cached_values.sync()
@@ -1523,7 +1501,7 @@ class QtDriver(DriverMixin, QObject):
item_limit: int = 10
path = Path(path)
- self.cached_values.beginGroup(SettingItems.LIBS_LIST)
+ self.cached_values.beginGroup(AppCacheItems.LIBS_LIST)
all_libs = {str(time.time()): str(path)}
@@ -1550,7 +1528,7 @@ class QtDriver(DriverMixin, QObject):
lib_items: dict[str, tuple[str, str]] = {}
# get recent libraries sorted by timestamp
- self.cached_values.beginGroup(SettingItems.LIBS_LIST)
+ self.cached_values.beginGroup(AppCacheItems.LIBS_LIST)
for item_tstamp in self.cached_values.allKeys():
val = str(self.cached_values.value(item_tstamp, type=str))
cut_val = val
@@ -1571,8 +1549,8 @@ class QtDriver(DriverMixin, QObject):
def clear_recent_libs(self):
"""Clear the list of recent libraries from the settings file."""
- settings = self.cached_values
- settings.beginGroup(SettingItems.LIBS_LIST)
+ cache = self.cached_values
+ cache.beginGroup(AppCacheItems.LIBS_LIST)
self.cached_values.remove("")
self.cached_values.endGroup()
self.cached_values.sync()
@@ -1581,6 +1559,23 @@ class QtDriver(DriverMixin, QObject):
def open_settings_modal(self):
SettingsPanel.build_modal(self).show()
+ def check_for_update(self):
+ """Check for an update to TagStudio and display a message box if there is one."""
+ latest_version = TagStudioCore.get_most_recent_release_version()
+ if latest_version == str(self.cached_values.value(AppCacheItems.DISMISSED_UPDATE)):
+ return
+
+ if latest_version and is_version_outdated(VERSION, latest_version):
+ update_box = UpdateAvailableMessageBox()
+ update_box.button(QMessageBox.StandardButton.Ignore).clicked.connect(
+ partial(self.dismiss_update, str(latest_version))
+ )
+ update_box.exec()
+
+ def dismiss_update(self, version: str):
+ """Dismiss an update notification for a specific new version of TagStudio."""
+ self.cached_values.setValue(AppCacheItems.DISMISSED_UPDATE, version)
+
def open_library(self, path: Path) -> None:
"""Open a TagStudio library."""
library_dir_display = (
diff --git a/src/tagstudio/qt/views/preview_panel_view.py b/src/tagstudio/qt/views/preview_panel_view.py
index 266e06fe..1e8640eb 100644
--- a/src/tagstudio/qt/views/preview_panel_view.py
+++ b/src/tagstudio/qt/views/preview_panel_view.py
@@ -2,28 +2,33 @@
# SPDX-License-Identifier: GPL-3.0-only
+import math
import traceback
import typing
from pathlib import Path
import structlog
from PySide6.QtCore import Qt
+from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QHBoxLayout,
+ QLabel,
QPushButton,
QSplitter,
QVBoxLayout,
QWidget,
)
+from tagstudio.core.constants import FFMPEG_HELP_URL
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.preview_thumb_controller import PreviewThumb
from tagstudio.qt.mixed.field_containers import FieldContainers
from tagstudio.qt.mixed.file_attributes import FileAttributeData, FileAttributes
+from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
-from tagstudio.qt.views.stylesheets.stylesheets import button_style
+from tagstudio.qt.views.stylesheets.stylesheets import button_style, preview_warning_style
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
@@ -39,9 +44,10 @@ class PreviewPanelView(QWidget):
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__()
self.lib = library
+ rm = ResourceManager()
- self.__thumb = PreviewThumb(self.lib, driver)
- self.__file_attrs = FileAttributes(self.lib, driver)
+ self._thumb = PreviewThumb(self.lib, driver)
+ self._file_attrs = FileAttributes(self.lib, driver)
self._containers = FieldContainers(
self.lib, driver
) # TODO: this should be name mangled, but is still needed on the controller side atm
@@ -51,6 +57,33 @@ class PreviewPanelView(QWidget):
preview_layout.setContentsMargins(0, 0, 0, 0)
preview_layout.setSpacing(6)
+ self._ffmpeg_warning_widget = QWidget()
+ self._ffmpeg_warning_widget.setObjectName("ffmpeg_widget")
+ ffmpeg_warning_layout = QHBoxLayout(self._ffmpeg_warning_widget)
+ ffmpeg_warning_layout.setContentsMargins(3, 3, 3, 3)
+ self._ffmpeg_warning_widget.setStyleSheet(preview_warning_style())
+ ffmpeg_warning_label = QLabel(
+ Translations.format(
+ "preview.missing_module.multimedia",
+ module=f'FFmpeg',
+ )
+ )
+ ffmpeg_warning_label.setWordWrap(True)
+ ffmpeg_warning_label.linkActivated.connect(
+ lambda x: QDesktopServices.openUrl(FFMPEG_HELP_URL)
+ )
+ warning_icon = QLabel()
+ warning_icon_pixmap = rm.alert.scaled(
+ math.floor(20 * self.devicePixelRatio()), math.floor(20 * self.devicePixelRatio())
+ )
+ warning_icon_pixmap.setDevicePixelRatio(self.devicePixelRatio())
+ warning_icon.setPixmap(warning_icon_pixmap)
+ ffmpeg_warning_layout.addWidget(warning_icon)
+ ffmpeg_warning_layout.addWidget(ffmpeg_warning_label)
+ ffmpeg_warning_layout.setStretch(1, 1)
+
+ self._ffmpeg_warning_widget.hide()
+
info_section = QWidget()
info_layout = QVBoxLayout(info_section)
info_layout.setContentsMargins(0, 0, 0, 0)
@@ -80,8 +113,9 @@ class PreviewPanelView(QWidget):
add_buttons_layout.addWidget(self.__add_tag_button)
add_buttons_layout.addWidget(self.__add_field_button)
- preview_layout.addWidget(self.__thumb)
- info_layout.addWidget(self.__file_attrs)
+ preview_layout.addWidget(self._thumb)
+ info_layout.addWidget(self._ffmpeg_warning_widget)
+ info_layout.addWidget(self._file_attrs)
info_layout.addWidget(self._containers)
splitter.addWidget(preview_section)
@@ -120,9 +154,9 @@ class PreviewPanelView(QWidget):
try:
# No Items Selected
if len(selected) == 0:
- self.__thumb.hide_preview()
- self.__file_attrs.update_stats()
- self.__file_attrs.update_date_label()
+ self._thumb.hide_preview()
+ self._file_attrs.update_stats()
+ self._file_attrs.update_date_label()
self._containers.hide_containers()
self.add_buttons_enabled = False
@@ -135,9 +169,9 @@ class PreviewPanelView(QWidget):
filepath: Path = unwrap(self.lib.library_dir) / entry.path
if update_preview:
- stats: FileAttributeData = self.__thumb.display_file(filepath)
- self.__file_attrs.update_stats(filepath, stats)
- self.__file_attrs.update_date_label(filepath)
+ stats: FileAttributeData = self._thumb.display_file(filepath)
+ self._file_attrs.update_stats(filepath, stats)
+ self._file_attrs.update_date_label(filepath)
self._containers.update_from_entry(entry_id)
self._set_selection_callback()
@@ -147,9 +181,9 @@ class PreviewPanelView(QWidget):
# Multiple Selected Items
elif len(selected) > 1:
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
- self.__thumb.hide_preview() # TODO: Render mixed selection
- self.__file_attrs.update_multi_selection(len(selected))
- self.__file_attrs.update_date_label()
+ self._thumb.hide_preview() # TODO: Render mixed selection
+ self._file_attrs.update_multi_selection(len(selected))
+ self._file_attrs.update_date_label()
self._containers.hide_containers() # TODO: Allow for mixed editing
self._set_selection_callback()
@@ -175,7 +209,7 @@ class PreviewPanelView(QWidget):
@property
def _file_attributes_widget(self) -> FileAttributes: # needed for the tests
"""Getter for the file attributes widget."""
- return self.__file_attrs
+ return self._file_attrs
@property
def field_containers_widget(self) -> FieldContainers: # needed for the tests
@@ -184,4 +218,4 @@ class PreviewPanelView(QWidget):
@property
def preview_thumb(self) -> PreviewThumb:
- return self.__thumb
+ return self._thumb
diff --git a/src/tagstudio/qt/views/preview_thumb_view.py b/src/tagstudio/qt/views/preview_thumb_view.py
index 381ae10b..be55eac2 100644
--- a/src/tagstudio/qt/views/preview_thumb_view.py
+++ b/src/tagstudio/qt/views/preview_thumb_view.py
@@ -8,7 +8,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, override
import structlog
-from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt
+from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt, Signal
from PySide6.QtGui import QAction, QMovie, QPixmap, QResizeEvent
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QStackedLayout, QWidget
@@ -33,6 +33,8 @@ THUMB_SIZE_FACTOR = 2
class PreviewThumbView(QWidget):
"""The Preview Panel Widget."""
+ check_ffmpeg = Signal(bool)
+
__img_button_size: tuple[int, int]
__image_ratio: float
@@ -133,12 +135,7 @@ class PreviewThumbView(QWidget):
def __thumb_renderer_updated_ratio_callback(self, ratio: float) -> None:
self.__image_ratio = ratio
- self.__update_image_size(
- (
- self.size().width(),
- self.size().height(),
- )
- )
+ self.__update_image_size((self.size().width(), self.size().height()))
def __stacked_page_setup(self, page: QWidget, widget: QWidget) -> None:
layout = QHBoxLayout(page)
@@ -186,9 +183,11 @@ class PreviewThumbView(QWidget):
if preview in [MediaType.AUDIO, MediaType.VIDEO]:
self.__media_player.show()
self.__image_layout.setCurrentWidget(self.__media_player_page)
+ self.check_ffmpeg.emit(True) # noqa: FBT003
else:
self.__media_player.stop()
self.__media_player.hide()
+ self.check_ffmpeg.emit(False) # noqa: FBT003
if preview in [MediaType.IMAGE, MediaType.AUDIO]:
self.__button_wrapper.show()
diff --git a/src/tagstudio/qt/views/stylesheets/stylesheets.py b/src/tagstudio/qt/views/stylesheets/stylesheets.py
index 542ebd3f..162e1868 100644
--- a/src/tagstudio/qt/views/stylesheets/stylesheets.py
+++ b/src/tagstudio/qt/views/stylesheets/stylesheets.py
@@ -372,6 +372,15 @@ def title_line_edit_style() -> str:
"""
+def preview_warning_style() -> str:
+ return f"""
+ QWidget#ffmpeg_widget {{
+ background: {get_ui_color(ColorType.DARK_ACCENT, UiColor.RED)};
+ border-radius: 6px;
+ }}
+ """
+
+
def header(string: str, level: int, color: str | None = None) -> str:
"""Wrap a string in HTML header tags.
diff --git a/src/tagstudio/resources/qt/images/alert.png b/src/tagstudio/resources/qt/images/alert.png
new file mode 100644
index 00000000..a7c3d02c
Binary files /dev/null and b/src/tagstudio/resources/qt/images/alert.png differ
diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json
index 9582cf33..5ddcf101 100644
--- a/src/tagstudio/resources/translations/en.json
+++ b/src/tagstudio/resources/translations/en.json
@@ -71,7 +71,6 @@
"entries.unlinked.search_and_relink": "&Search && Relink",
"entries.unlinked.title": "Fix Unlinked Entries",
"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",
@@ -137,6 +136,7 @@
"generic.delete_alt": "&Delete",
"generic.done": "Done",
"generic.done_alt": "&Done",
+ "generic.dont_remind": "Don’t Remind Me Again",
"generic.edit": "Edit",
"generic.edit_alt": "&Edit",
"generic.filename": "Filename",
@@ -268,6 +268,8 @@
"namespace.new.button": "New Namespace",
"namespace.new.prompt": "Create a New Namespace to Start Adding Custom Colors!",
"preview.ignored": "Ignored",
+ "preview.missing_module.jxl": "{module} is required for JPEG XL previews",
+ "preview.missing_module.multimedia": "{module} is required for multimedia playback",
"preview.multiple_selection": "{count} Items Selected",
"preview.no_selection": "No Items Selected",
"preview.unlinked": "Unlinked",
@@ -372,6 +374,7 @@
"trash.dialog.title.singular": "Delete File",
"trash.name.generic": "Trash",
"trash.name.windows": "Recycle Bin",
+ "update.view_update": "View Update",
"version_modal.description": "A new version of TagStudio is available! You can download the latest release from GitHub.",
"version_modal.status": "Installed Version: {installed_version}
Latest Release Version: {latest_release_version}",
"version_modal.title": "TagStudio Update Available",
diff --git a/tests/test_driver.py b/tests/test_driver.py
index 5ca04b04..b4ce1941 100644
--- a/tests/test_driver.py
+++ b/tests/test_driver.py
@@ -7,7 +7,7 @@ from pathlib import Path
from PySide6.QtCore import QSettings
from tagstudio.core.driver import DriverMixin
-from tagstudio.core.enums import SettingItems
+from tagstudio.core.enums import AppCacheItems
from tagstudio.core.library.alchemy.library import LibraryStatus
from tagstudio.qt.global_settings import GlobalSettings
@@ -43,7 +43,7 @@ def test_evaluate_path_missing():
def test_evaluate_path_last_lib_not_exists():
# Given
cache = QSettings()
- cache.setValue(SettingItems.LAST_LIBRARY, "/0/4/5/1/")
+ cache.setValue(AppCacheItems.LAST_LIBRARY, "/0/4/5/1/")
driver = TestDriver(GlobalSettings(), cache)
# When
@@ -57,7 +57,7 @@ def test_evaluate_path_last_lib_present(library_dir: Path):
# Given
cache_file = library_dir / "test_settings.ini"
cache = QSettings(str(cache_file), QSettings.Format.IniFormat)
- cache.setValue(SettingItems.LAST_LIBRARY, library_dir)
+ cache.setValue(AppCacheItems.LAST_LIBRARY, library_dir)
cache.sync()
settings = GlobalSettings()