From 5eb3dc2130614fe91223e11e2b1850d759549227 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:07:01 -0700 Subject: [PATCH 1/5] feat: dismiss update notification, change ffmpeg to banner --- docs/assets/alert.png | Bin 0 -> 2817 bytes src/tagstudio/core/constants.py | 1 + src/tagstudio/core/driver.py | 8 +- src/tagstudio/core/enums.py | 3 +- .../controllers/ffmpeg_missing_message_box.py | 57 --------------- .../controllers/preview_panel_controller.py | 10 +++ ...box.py => update_available_message_box.py} | 20 +++-- src/tagstudio/qt/previews/renderer.py | 6 +- src/tagstudio/qt/resource_manager.pyi | 1 + src/tagstudio/qt/resources.json | 4 + src/tagstudio/qt/ts_qt.py | 69 ++++++++---------- src/tagstudio/qt/views/preview_panel_view.py | 66 +++++++++++++---- src/tagstudio/qt/views/preview_thumb_view.py | 13 ++-- .../qt/views/stylesheets/stylesheets.py | 9 +++ src/tagstudio/resources/qt/images/alert 2.png | Bin 0 -> 2820 bytes src/tagstudio/resources/qt/images/alert.png | Bin 0 -> 2817 bytes src/tagstudio/resources/translations/en.json | 5 +- tests/test_driver.py | 6 +- 18 files changed, 142 insertions(+), 136 deletions(-) create mode 100644 docs/assets/alert.png delete mode 100644 src/tagstudio/qt/controllers/ffmpeg_missing_message_box.py rename src/tagstudio/qt/controllers/{out_of_date_message_box.py => update_available_message_box.py} (73%) create mode 100644 src/tagstudio/resources/qt/images/alert 2.png create mode 100644 src/tagstudio/resources/qt/images/alert.png diff --git a/docs/assets/alert.png b/docs/assets/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..a7c3d02cbe688c1433529abb4d432465cbd81a83 GIT binary patch literal 2817 zcmai02~?8l8b(Vr9LtkSWiA{tH$)b5BhgfF$6Rx%a}nVON#Q5JM9m`0%*Hg^oJsVi zEmLMCS*9(fP1%HN*Gelbw3M<%OPjH>yg%4jPId1&Y~S}h@AGco`~BpGhWP0rOb{>_ zOfSHn91eZQtKK?r=(GQqR96^m_E}D3j3|a0Ok{vu2Rai>035_zK7_+y?w(>kosk5H zkO=^r!y}=FFJ3_*IZP5N+KGyz@_m3rj(@rUh)55KWTYoC+?Xg&4}`mz2nlcj5gjS! zCi8?uF$tyCB|^Vd+gKD*O%WxLP%+d{qz@b)N8Z`cNUTDg=vnz+u0pi#V(w&{fDcbbcZz0)>ene>#B~ zE~epc459LU-$YF3e~(P1{%<;$J0lIDh_W6k#OI>Ui0N+!!pJl}fDH$PV2XeNP}T!H z(GqnfRCOVSa>PJ#44K0PctWVUB$S);Kfu@@fIeU{D2RX#6+lAa)i$ci5d*k%HW1BW ziV~-_zU&7BJoY~URbQ9&jYoTFOs z3l!kv6Dj~%oMZ?ShWmLV11LVucsFNf48dW_1JpDkWXoZtk?A6cMZgi9F*pc!i^SoH zPOd~e0fTcP;&5tZH5-}*CS64Te|*)6M!G{5R4UP*BNTyxH1*chF^>S&O|7Pu$sG0h zMk3W+Cej(IB9Tyh0mw{Y08F)WNNx%hf-F%gT>yBop$3ysUMv;|DrXuJ8XXRs2MCY^ zBmtUBrYJ%XLSNT9{^RRr*$ z`$qs>H1^2bxG=sIafaT>hYz+R{Kj!3cKwd<^@Rb6E?v(xz0zCqm9tBWUON;ddHIwgm)44 zL8t7h+f3j=mi`HSMk$nv89)}-&Wke6~lTsE_->0>w7T**)V zpj_CqlK*R)5^Hg_A?NApw39Dk?u+>w_jREz@?~>&7NmFUur1HmqRn&8M;z1h`luI) zVP@=qpnRf{_54j#SJnA>W`c*+2EPYwxdBU7xm}7zU-Hw#uOMbQuw)ee#kitB*E0OVHX9~~A$<{srxYqCdx_xQZ zP3?{l&n!!%uJMk99&bijUP+e8ECjs#`ttMJ7dkG_Xlr@VJG!^1SJ<|ky=nB-3s8AE ze&IIyj8{{LGXK6--cr|Li9rKhv3Xo+Aqi^8<~_|J?i;`t!)GgpHESdbK;hGj26S)7 zHa}&j(m+=$C=EcTj<&2%Wam)fdWz zF~i&Hiz@vN99^t0rOootHgEpvwblsPWz6MC#qJ|-Z4Z!BAw@$Yo zJ|cD*qZ!LngW|#(+~jx97zokRj5*@*zV*>7&-6yCa|Cmcr(x z_)M~oO!UN}*FA~yox9OxY%=54Y~`f6!CY6pOZcPkfC{Qt!ug8vYHS;g^NUZA;JwmvG*Mq!9X^Ld6cY}8_XZd2DXp^ebW^>ljI(UX#J$l1_y z<8yFwvFzCbhQU3Zwi73NoQy*6vu=C8y|+&ei#ogLTxd1+y2`ZrKkF7Nk|&bO6K&$p zwz?Wbn!1NYc-+r>)UmDeNTWq%@Px$?%|P@)qv9~#bsxfmH4IW6+w^3;MJPS}?p9Zq zYwKx&RIiiw^WwH+2-fHoe3!>P`u8$z%OrQXz8Lb0<^V*fif1)kFMZETTvr@6-rI3J z`9V?p0Q`^AELNi9TKvuWmbDFe5dF-{<~3R^cUZSk59&>OC1&xCYYPF|piTTMD5uyz z2adtXNj&TBytw4qN((dL)flZ8hQ(n&J1Khc)|S|7S)(hf-w${{{?imX@!|5#j8lUK zzh1X5EDE1z+9_GGDBFJD@5i;Rs9vvgmEgSr`#l*ErF-u$s4x#`KfK#SfBC!ThXwnu zW1%4`6(|;Jt!c4m*D jNgOyKe>-E=caF|Gxnm?>qXcMF{c{bVgpf~p$7lT)U`$;% literal 0 HcmV?d00001 diff --git a/src/tagstudio/core/constants.py b/src/tagstudio/core/constants.py index 7b323c8c7..a746f038e 100644 --- a/src/tagstudio/core/constants.py +++ b/src/tagstudio/core/constants.py @@ -11,6 +11,7 @@ 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 cf50c45ee..4d73a92a3 100644 --- a/src/tagstudio/core/driver.py +++ b/src/tagstudio/core/driver.py @@ -8,7 +8,7 @@ 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 @@ def evaluate_path(self, open_path: str | None) -> LibraryStatus: 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 c2d6a9c2c..ae8bf8bc9 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 a3c711760..000000000 --- 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 d3852a805..8c2761325 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 @@ def __init__(self, library: Library, driver: "QtDriver") -> None: 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 @@ def _add_tag_to_selected(self, tag_id: int) -> None: 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 71412f1eb..a94156732 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 @@ 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 @@ def __init__(self): 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.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.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.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 b7f6232d4..ee087384d 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -98,9 +98,9 @@ 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 be327a0ff..0d497c8f5 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" + alert: QPixmap about_bg: QPixmap adobe_illustrator: Image.Image adobe_photoshop: Image.Image diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 48b6f8f32..6533ce38c 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 a5a9c840e..56e45bdf8 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -17,9 +17,9 @@ 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 @@ 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.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.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 @@ 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 @@ def on_visible_changed(entry_id: int | None): 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 @@ def close_library(self, is_shutdown: bool = False): 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 @@ def update_browsing_state(self, state: BrowsingState | None = None) -> None: ) 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 @@ def update_libs_list(self, path: Path | str): 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 @@ def update_recent_lib_menu(self): 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 @@ def update_recent_lib_menu(self): 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 @@ def clear_recent_libs(self): 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.ignore_update, str(latest_version)) + ) + update_box.exec() + + def ignore_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 266e06fe0..1e8640eb3 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 @@ def __init__(self, library: Library, driver: "QtDriver") -> None: 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 @@ def __init__(self, library: Library, driver: "QtDriver") -> None: 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 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non 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 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non 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 @@ def set_selection(self, selected: list[int], update_preview: bool = True) -> Non # 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 @@ def add_buttons_enabled(self, enabled: bool) -> None: @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 @@ def field_containers_widget(self) -> FieldContainers: # needed for the tests @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 381ae10bf..be55eac2d 100644 --- a/src/tagstudio/qt/views/preview_thumb_view.py +++ b/src/tagstudio/qt/views/preview_thumb_view.py @@ -8,7 +8,7 @@ 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 @@ class PreviewThumbView(QWidget): """The Preview Panel Widget.""" + check_ffmpeg = Signal(bool) + __img_button_size: tuple[int, int] __image_ratio: float @@ -133,12 +135,7 @@ def __thumb_renderer_updated_callback( 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 @@ def __switch_preview(self, preview: MediaType | None) -> None: 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 542ebd3fa..162e1868b 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 2.png b/src/tagstudio/resources/qt/images/alert 2.png new file mode 100644 index 0000000000000000000000000000000000000000..444fda9dcd7087bd09b41da19980b4edb12a1322 GIT binary patch literal 2820 zcmai03p|s1AKzRSldg1JDmx>I*lfpTJ0xtmg)wsJbeQcKW4CvE*rX)2R~IUkE-vLH zm1`(UF6j-O+`1{sB@|ACRjLzC-e=RHPW8T@&+|Nc{{Qdy_x;`e|NpbS6b~nLqz)1W zgQ>eXlf0qdVA-vLfS!FPMAk5v(q)#9Kj`nikx1up%xDZ=2w*1W@F5%qv#}HNY4ivH zM1=sMEUqnjtl~Nv#bVf^{Vd$E?tDiejODyl08qDj_|UgT&k^@N*)#@?l2gD4TeQDB1?9*S04NJH3o|Tw9TH_DU@(c^qz#{pp^+^* z38YDnS7qIvsi^oOD zG-)BcNYECIhH%s;aX`$PQ|Aglr4JPXBSSFeW?0PUbdbgT4qb+PMdyd{K%Ov+$Dd8$ z3m3ESSB6mezHK6=@xMiOcmLmX4(E$BgdjNzD#WLvei75(4un3@d;sGO2zikLIzWyB zxS+ng60*7wDJ(I-_9wA809OcA*A`8%{0A8D9ng`-<_W0Kq5^Eu=5iZZ<%ljES}5Sh zVt`?@T66Oo0dD9&0a;}1NLg!rxhyCb+?(wtyvX^JzO@c=cA2-&ij(IgrOv2a+Nr70G|2|ieJ zq6LKCqoA$B3grR< z6b^-h_NB#_R-!NfQv7~?m`~ARWV`O~@N6(&C14{be6~#?yO}v?`$OjlX7(U~2D3*I zz=h5q0d&y(K-Q6d4R+2RmPFHFD7++ft?ptP9LTb$<*DhYdNvtYvDL7*&A?GLhcN%^y zC*9o!8yVMkRwxYq&8Qw5wpL?Aqg9K7Jya|nT(+3b483#mWL3fEwj%>q?~uaZDLP5Qy?zT&RvL?f zKZT1t=D)=kogYqUe2{neJfK|JFn(iRyL$SPJy{M%AmI_^YDAQZq!RgZc%t_(#l}3) zx5iMg%QLZbjh1KPfX<@IpXb{YESlg;J@iKnQd8sm_Ps|JIjyO?Qln^EaA-5NMs*ya z7mVTmV*W8K)oZ&rTo0@q^kHUN;(aIkQTcooV-H?Q&h^*9P}?E5wT?ciu4) z+5e)r^2J1Nqr(TTvkB7L@p0gzkd`4I-g@aMq30DcXNz>ySyI{N6^TT4k1!%5#D3XJ zVwFb6CVDTrekjl0)c1R1;_|6Ny^rtDsCG*h_l_bw6fP#^*dGq3Ql#ABn{5LOY2h)2 zyx4h_3tr}**s;IE|Dm_USbdM>vr86&r*Zl>Ds-$A`gIp^>J7Q@>AI8to4vw~f^(ZM zkToYu!W7Rmw>Qz_ri1k#`6u2-9xBl;HR1*>i&%eIJ7{eRPvO=sQR>0ki&+e9+iz-uL+5_zI1yfGrbIUBCJ=~ zr5WiUwK(_|CCz!Btw#@9KC`;q3$moEO_olt&WXE^469?dI&@ZRP-|+t5`~WJ_u0;h zN>TV0_2RDM-c(ASW!AEg{Y|%#i#nUFtw>Qpu11-WX8Smtyl09vRW&2!#molH ro|qo$rfFh-Uc+0hf79l!q&ExW=OeZs3ZweU{=B-7JxGNP!SVkEq5Wmu literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/alert.png b/src/tagstudio/resources/qt/images/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..a7c3d02cbe688c1433529abb4d432465cbd81a83 GIT binary patch literal 2817 zcmai02~?8l8b(Vr9LtkSWiA{tH$)b5BhgfF$6Rx%a}nVON#Q5JM9m`0%*Hg^oJsVi zEmLMCS*9(fP1%HN*Gelbw3M<%OPjH>yg%4jPId1&Y~S}h@AGco`~BpGhWP0rOb{>_ zOfSHn91eZQtKK?r=(GQqR96^m_E}D3j3|a0Ok{vu2Rai>035_zK7_+y?w(>kosk5H zkO=^r!y}=FFJ3_*IZP5N+KGyz@_m3rj(@rUh)55KWTYoC+?Xg&4}`mz2nlcj5gjS! zCi8?uF$tyCB|^Vd+gKD*O%WxLP%+d{qz@b)N8Z`cNUTDg=vnz+u0pi#V(w&{fDcbbcZz0)>ene>#B~ zE~epc459LU-$YF3e~(P1{%<;$J0lIDh_W6k#OI>Ui0N+!!pJl}fDH$PV2XeNP}T!H z(GqnfRCOVSa>PJ#44K0PctWVUB$S);Kfu@@fIeU{D2RX#6+lAa)i$ci5d*k%HW1BW ziV~-_zU&7BJoY~URbQ9&jYoTFOs z3l!kv6Dj~%oMZ?ShWmLV11LVucsFNf48dW_1JpDkWXoZtk?A6cMZgi9F*pc!i^SoH zPOd~e0fTcP;&5tZH5-}*CS64Te|*)6M!G{5R4UP*BNTyxH1*chF^>S&O|7Pu$sG0h zMk3W+Cej(IB9Tyh0mw{Y08F)WNNx%hf-F%gT>yBop$3ysUMv;|DrXuJ8XXRs2MCY^ zBmtUBrYJ%XLSNT9{^RRr*$ z`$qs>H1^2bxG=sIafaT>hYz+R{Kj!3cKwd<^@Rb6E?v(xz0zCqm9tBWUON;ddHIwgm)44 zL8t7h+f3j=mi`HSMk$nv89)}-&Wke6~lTsE_->0>w7T**)V zpj_CqlK*R)5^Hg_A?NApw39Dk?u+>w_jREz@?~>&7NmFUur1HmqRn&8M;z1h`luI) zVP@=qpnRf{_54j#SJnA>W`c*+2EPYwxdBU7xm}7zU-Hw#uOMbQuw)ee#kitB*E0OVHX9~~A$<{srxYqCdx_xQZ zP3?{l&n!!%uJMk99&bijUP+e8ECjs#`ttMJ7dkG_Xlr@VJG!^1SJ<|ky=nB-3s8AE ze&IIyj8{{LGXK6--cr|Li9rKhv3Xo+Aqi^8<~_|J?i;`t!)GgpHESdbK;hGj26S)7 zHa}&j(m+=$C=EcTj<&2%Wam)fdWz zF~i&Hiz@vN99^t0rOootHgEpvwblsPWz6MC#qJ|-Z4Z!BAw@$Yo zJ|cD*qZ!LngW|#(+~jx97zokRj5*@*zV*>7&-6yCa|Cmcr(x z_)M~oO!UN}*FA~yox9OxY%=54Y~`f6!CY6pOZcPkfC{Qt!ug8vYHS;g^NUZA;JwmvG*Mq!9X^Ld6cY}8_XZd2DXp^ebW^>ljI(UX#J$l1_y z<8yFwvFzCbhQU3Zwi73NoQy*6vu=C8y|+&ei#ogLTxd1+y2`ZrKkF7Nk|&bO6K&$p zwz?Wbn!1NYc-+r>)UmDeNTWq%@Px$?%|P@)qv9~#bsxfmH4IW6+w^3;MJPS}?p9Zq zYwKx&RIiiw^WwH+2-fHoe3!>P`u8$z%OrQXz8Lb0<^V*fif1)kFMZETTvr@6-rI3J z`9V?p0Q`^AELNi9TKvuWmbDFe5dF-{<~3R^cUZSk59&>OC1&xCYYPF|piTTMD5uyz z2adtXNj&TBytw4qN((dL)flZ8hQ(n&J1Khc)|S|7S)(hf-w${{{?imX@!|5#j8lUK zzh1X5EDE1z+9_GGDBFJD@5i;Rs9vvgmEgSr`#l*ErF-u$s4x#`KfK#SfBC!ThXwnu zW1%4`6(|;Jt!c4m*D jNgOyKe>-E=caF|Gxnm?>qXcMF{c{bVgpf~p$7lT)U`$;% literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 9582cf33b..5ddcf1014 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 5ca04b046..b4ce19415 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -7,7 +7,7 @@ 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() From f92b187aa1d5cd8c1a7911d712f613bd2c3b8eb5 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:10:46 -0700 Subject: [PATCH 2/5] chore: sort resources.pyi list --- src/tagstudio/qt/resource_manager.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tagstudio/qt/resource_manager.pyi b/src/tagstudio/qt/resource_manager.pyi index 0d497c8f5..986ed8843 100644 --- a/src/tagstudio/qt/resource_manager.pyi +++ b/src/tagstudio/qt/resource_manager.pyi @@ -18,11 +18,11 @@ class ResourceManager: _instance: ResourceManager | None # Resources IDs from "resources.json" - alert: QPixmap about_bg: QPixmap 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 From 861f1b7f92fd4d8ade224195cb39d29a984c6d2f Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:13:53 -0700 Subject: [PATCH 3/5] fix: remove unused asset --- src/tagstudio/resources/qt/images/alert 2.png | Bin 2820 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/tagstudio/resources/qt/images/alert 2.png diff --git a/src/tagstudio/resources/qt/images/alert 2.png b/src/tagstudio/resources/qt/images/alert 2.png deleted file mode 100644 index 444fda9dcd7087bd09b41da19980b4edb12a1322..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2820 zcmai03p|s1AKzRSldg1JDmx>I*lfpTJ0xtmg)wsJbeQcKW4CvE*rX)2R~IUkE-vLH zm1`(UF6j-O+`1{sB@|ACRjLzC-e=RHPW8T@&+|Nc{{Qdy_x;`e|NpbS6b~nLqz)1W zgQ>eXlf0qdVA-vLfS!FPMAk5v(q)#9Kj`nikx1up%xDZ=2w*1W@F5%qv#}HNY4ivH zM1=sMEUqnjtl~Nv#bVf^{Vd$E?tDiejODyl08qDj_|UgT&k^@N*)#@?l2gD4TeQDB1?9*S04NJH3o|Tw9TH_DU@(c^qz#{pp^+^* z38YDnS7qIvsi^oOD zG-)BcNYECIhH%s;aX`$PQ|Aglr4JPXBSSFeW?0PUbdbgT4qb+PMdyd{K%Ov+$Dd8$ z3m3ESSB6mezHK6=@xMiOcmLmX4(E$BgdjNzD#WLvei75(4un3@d;sGO2zikLIzWyB zxS+ng60*7wDJ(I-_9wA809OcA*A`8%{0A8D9ng`-<_W0Kq5^Eu=5iZZ<%ljES}5Sh zVt`?@T66Oo0dD9&0a;}1NLg!rxhyCb+?(wtyvX^JzO@c=cA2-&ij(IgrOv2a+Nr70G|2|ieJ zq6LKCqoA$B3grR< z6b^-h_NB#_R-!NfQv7~?m`~ARWV`O~@N6(&C14{be6~#?yO}v?`$OjlX7(U~2D3*I zz=h5q0d&y(K-Q6d4R+2RmPFHFD7++ft?ptP9LTb$<*DhYdNvtYvDL7*&A?GLhcN%^y zC*9o!8yVMkRwxYq&8Qw5wpL?Aqg9K7Jya|nT(+3b483#mWL3fEwj%>q?~uaZDLP5Qy?zT&RvL?f zKZT1t=D)=kogYqUe2{neJfK|JFn(iRyL$SPJy{M%AmI_^YDAQZq!RgZc%t_(#l}3) zx5iMg%QLZbjh1KPfX<@IpXb{YESlg;J@iKnQd8sm_Ps|JIjyO?Qln^EaA-5NMs*ya z7mVTmV*W8K)oZ&rTo0@q^kHUN;(aIkQTcooV-H?Q&h^*9P}?E5wT?ciu4) z+5e)r^2J1Nqr(TTvkB7L@p0gzkd`4I-g@aMq30DcXNz>ySyI{N6^TT4k1!%5#D3XJ zVwFb6CVDTrekjl0)c1R1;_|6Ny^rtDsCG*h_l_bw6fP#^*dGq3Ql#ABn{5LOY2h)2 zyx4h_3tr}**s;IE|Dm_USbdM>vr86&r*Zl>Ds-$A`gIp^>J7Q@>AI8to4vw~f^(ZM zkToYu!W7Rmw>Qz_ri1k#`6u2-9xBl;HR1*>i&%eIJ7{eRPvO=sQR>0ki&+e9+iz-uL+5_zI1yfGrbIUBCJ=~ zr5WiUwK(_|CCz!Btw#@9KC`;q3$moEO_olt&WXE^469?dI&@ZRP-|+t5`~WJ_u0;h zN>TV0_2RDM-c(ASW!AEg{Y|%#i#nUFtw>Qpu11-WX8Smtyl09vRW&2!#molH ro|qo$rfFh-Uc+0hf79l!q&ExW=OeZs3ZweU{=B-7JxGNP!SVkEq5Wmu From a2dfe6ba9e6b09a2f23177f83450c2caf469ac1f Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:14:35 -0700 Subject: [PATCH 4/5] refactor: rename dismiss_update() --- src/tagstudio/qt/ts_qt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 56e45bdf8..50dda1af8 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -1568,11 +1568,11 @@ def check_for_update(self): if latest_version and is_version_outdated(VERSION, latest_version): update_box = UpdateAvailableMessageBox() update_box.button(QMessageBox.StandardButton.Ignore).clicked.connect( - partial(self.ignore_update, str(latest_version)) + partial(self.dismiss_update, str(latest_version)) ) update_box.exec() - def ignore_update(self, version: str): + 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) From a9f16bf74895890b120e0339ca98b08810ac32f0 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:17:56 -0700 Subject: [PATCH 5/5] fix: remove *other* unused asset --- docs/assets/alert.png | Bin 2817 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/assets/alert.png diff --git a/docs/assets/alert.png b/docs/assets/alert.png deleted file mode 100644 index a7c3d02cbe688c1433529abb4d432465cbd81a83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2817 zcmai02~?8l8b(Vr9LtkSWiA{tH$)b5BhgfF$6Rx%a}nVON#Q5JM9m`0%*Hg^oJsVi zEmLMCS*9(fP1%HN*Gelbw3M<%OPjH>yg%4jPId1&Y~S}h@AGco`~BpGhWP0rOb{>_ zOfSHn91eZQtKK?r=(GQqR96^m_E}D3j3|a0Ok{vu2Rai>035_zK7_+y?w(>kosk5H zkO=^r!y}=FFJ3_*IZP5N+KGyz@_m3rj(@rUh)55KWTYoC+?Xg&4}`mz2nlcj5gjS! zCi8?uF$tyCB|^Vd+gKD*O%WxLP%+d{qz@b)N8Z`cNUTDg=vnz+u0pi#V(w&{fDcbbcZz0)>ene>#B~ zE~epc459LU-$YF3e~(P1{%<;$J0lIDh_W6k#OI>Ui0N+!!pJl}fDH$PV2XeNP}T!H z(GqnfRCOVSa>PJ#44K0PctWVUB$S);Kfu@@fIeU{D2RX#6+lAa)i$ci5d*k%HW1BW ziV~-_zU&7BJoY~URbQ9&jYoTFOs z3l!kv6Dj~%oMZ?ShWmLV11LVucsFNf48dW_1JpDkWXoZtk?A6cMZgi9F*pc!i^SoH zPOd~e0fTcP;&5tZH5-}*CS64Te|*)6M!G{5R4UP*BNTyxH1*chF^>S&O|7Pu$sG0h zMk3W+Cej(IB9Tyh0mw{Y08F)WNNx%hf-F%gT>yBop$3ysUMv;|DrXuJ8XXRs2MCY^ zBmtUBrYJ%XLSNT9{^RRr*$ z`$qs>H1^2bxG=sIafaT>hYz+R{Kj!3cKwd<^@Rb6E?v(xz0zCqm9tBWUON;ddHIwgm)44 zL8t7h+f3j=mi`HSMk$nv89)}-&Wke6~lTsE_->0>w7T**)V zpj_CqlK*R)5^Hg_A?NApw39Dk?u+>w_jREz@?~>&7NmFUur1HmqRn&8M;z1h`luI) zVP@=qpnRf{_54j#SJnA>W`c*+2EPYwxdBU7xm}7zU-Hw#uOMbQuw)ee#kitB*E0OVHX9~~A$<{srxYqCdx_xQZ zP3?{l&n!!%uJMk99&bijUP+e8ECjs#`ttMJ7dkG_Xlr@VJG!^1SJ<|ky=nB-3s8AE ze&IIyj8{{LGXK6--cr|Li9rKhv3Xo+Aqi^8<~_|J?i;`t!)GgpHESdbK;hGj26S)7 zHa}&j(m+=$C=EcTj<&2%Wam)fdWz zF~i&Hiz@vN99^t0rOootHgEpvwblsPWz6MC#qJ|-Z4Z!BAw@$Yo zJ|cD*qZ!LngW|#(+~jx97zokRj5*@*zV*>7&-6yCa|Cmcr(x z_)M~oO!UN}*FA~yox9OxY%=54Y~`f6!CY6pOZcPkfC{Qt!ug8vYHS;g^NUZA;JwmvG*Mq!9X^Ld6cY}8_XZd2DXp^ebW^>ljI(UX#J$l1_y z<8yFwvFzCbhQU3Zwi73NoQy*6vu=C8y|+&ei#ogLTxd1+y2`ZrKkF7Nk|&bO6K&$p zwz?Wbn!1NYc-+r>)UmDeNTWq%@Px$?%|P@)qv9~#bsxfmH4IW6+w^3;MJPS}?p9Zq zYwKx&RIiiw^WwH+2-fHoe3!>P`u8$z%OrQXz8Lb0<^V*fif1)kFMZETTvr@6-rI3J z`9V?p0Q`^AELNi9TKvuWmbDFe5dF-{<~3R^cUZSk59&>OC1&xCYYPF|piTTMD5uyz z2adtXNj&TBytw4qN((dL)flZ8hQ(n&J1Khc)|S|7S)(hf-w${{{?imX@!|5#j8lUK zzh1X5EDE1z+9_GGDBFJD@5i;Rs9vvgmEgSr`#l*ErF-u$s4x#`KfK#SfBC!ThXwnu zW1%4`6(|;Jt!c4m*D jNgOyKe>-E=caF|Gxnm?>qXcMF{c{bVgpf~p$7lT)U`$;%