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()