feat: update notification (#1166)

* feat: update notification

* fix: missing dependency

* fix: replace custom parsing with semver dependency

* fix: link directly to latest release
This commit is contained in:
Jann Stute
2025-12-15 19:00:46 +01:00
committed by GitHub
parent 04744b224c
commit f3bcb7c5c6
7 changed files with 113 additions and 7 deletions

View File

@@ -33,6 +33,8 @@ dependencies = [
"typing_extensions~=4.13",
"ujson~=5.10",
"wcmatch==10.*",
"requests~=2.31.0",
"semver~=3.0.4",
]
[project.optional-dependencies]

View File

@@ -5,17 +5,23 @@
"""The core classes and methods of TagStudio."""
import json
import re
from functools import lru_cache
from pathlib import Path
import requests
import structlog
from tagstudio.core.constants import TS_FOLDER_NAME
from tagstudio.core.library.alchemy.fields import FieldID
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap
logger = structlog.get_logger(__name__)
MOST_RECENT_RELEASE_VERSION: str | None = None
class TagStudioCore:
def __init__(self):
@@ -27,6 +33,7 @@ class TagStudioCore:
Return a formatted object with notable values or an empty object if none is found.
"""
raise NotImplementedError("This method is currently broken and needs to be fixed.")
info = {}
_filepath = filepath.parent / (filepath.name + ".json")
@@ -101,11 +108,11 @@ class TagStudioCore:
"""Match defined conditions against a file to add Entry data."""
# TODO - what even is this file format?
# TODO: Make this stored somewhere better instead of temporarily in this JSON file.
cond_file = lib.library_dir / TS_FOLDER_NAME / "conditions.json"
cond_file = unwrap(lib.library_dir) / TS_FOLDER_NAME / "conditions.json"
if not cond_file.is_file():
return False
entry: Entry = lib.get_entry(entry_id)
entry: Entry = unwrap(lib.get_entry(entry_id))
try:
with open(cond_file, encoding="utf8") as f:
@@ -130,7 +137,9 @@ class TagStudioCore:
is_new = field["id"] not in entry_field_types
field_key = field["id"]
if is_new:
lib.add_field_to_entry(entry.id, field_key, field["value"])
lib.add_field_to_entry(
entry.id, field_id=field_key, value=field["value"]
)
else:
lib.update_entry_field(entry.id, field_key, field["value"])
@@ -181,3 +190,21 @@ class TagStudioCore:
except Exception:
logger.exception("Error building Instagram URL.", entry=entry)
return ""
@staticmethod
@lru_cache(maxsize=1)
def get_most_recent_release_version() -> str:
"""Get the version of the most recent Github release."""
resp = requests.get("https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest")
assert resp.status_code == 200, "Could not fetch information on latest release."
data = resp.json()
tag: str = data["tag_name"]
assert tag.startswith("v")
version = tag[1:]
# the assert does not allow for prerelease/build,
# because the latest release should never have them
assert re.match(r"^\d+\.\d+\.\d+$", version) is not None, "Invalid version format."
return version

View File

@@ -2,6 +2,8 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import semver
def strip_punctuation(string: str) -> str:
"""Returns a given string stripped of all punctuation characters."""
@@ -32,3 +34,18 @@ def strip_web_protocol(string: str) -> str:
for prefix in prefixes:
string = string.removeprefix(prefix)
return string
def is_version_outdated(current: str, latest: str) -> bool:
vcur = semver.Version.parse(current)
vlat = semver.Version.parse(latest)
assert vlat.prerelease is None and vlat.build is None
if vcur.major != vlat.major:
return vcur.major < vlat.major
elif vcur.minor != vlat.minor:
return vcur.minor < vlat.minor
elif vcur.patch != vlat.patch:
return vcur.patch < vlat.patch
else:
return vcur.prerelease is not None or vcur.build is not None

View File

@@ -0,0 +1,39 @@
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMessageBox
from tagstudio.core.constants import VERSION
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
logger = structlog.get_logger(__name__)
class OutOfDateMessageBox(QMessageBox):
"""A warning dialog for if the TagStudio is not running under the latest release version."""
def __init__(self):
super().__init__()
title = Translations.format("version_modal.title")
self.setWindowTitle(title)
self.setIcon(QMessageBox.Icon.Warning)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setStandardButtons(
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()
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
latest_release_version = TagStudioCore.get_most_recent_release_version()
status = Translations.format(
"version_modal.status",
installed_version=f"<span style='color:{red}'>{VERSION}</span>",
latest_release_version=f"<span style='color:{green}'>{latest_release_version}</span>",
)
self.setText(f"{Translations['version_modal.description']}<br><br>{status}")

View File

@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
from tagstudio.core.constants import VERSION, VERSION_BRANCH
from tagstudio.core.enums import Theme
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.previews.vendored import ffmpeg
from tagstudio.qt.resource_manager import ResourceManager
@@ -103,6 +104,19 @@ class AboutModal(QWidget):
self.system_info_layout = QFormLayout(self.system_info_widget)
self.system_info_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
# Version
version_title = QLabel("Version")
most_recent_release = TagStudioCore.get_most_recent_release_version()
version_content_style = self.form_content_style
if most_recent_release == VERSION:
version_content = QLabel(f"{VERSION}")
else:
version_content = QLabel(f"{VERSION} (Latest Release: {most_recent_release})")
version_content_style += "color: #d9534f;"
version_content.setStyleSheet(version_content_style)
version_content.setMaximumWidth(version_content.sizeHint().width())
self.system_info_layout.addRow(version_title, version_content)
# License
license_title = QLabel(f"{Translations['about.license']}")
license_content = QLabel("GPLv3")

View File

@@ -62,7 +62,7 @@ from tagstudio.core.library.refresh import RefreshTracker
from tagstudio.core.media_types import MediaCategories
from tagstudio.core.query_lang.util import ParsingError
from tagstudio.core.ts_core import TagStudioCore
from tagstudio.core.utils.str_formatting import strip_web_protocol
from tagstudio.core.utils.str_formatting import is_version_outdated, strip_web_protocol
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
@@ -71,6 +71,7 @@ from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMes
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.global_settings import (
DEFAULT_GLOBAL_SETTINGS_PATH,
GlobalSettings,
@@ -579,7 +580,7 @@ class QtDriver(DriverMixin, QObject):
)
self.init_library_window()
self.migration_modal: JsonMigrationModal = None
self.migration_modal: JsonMigrationModal | None = None
path_result = self.evaluate_path(str(self.args.open).lstrip().rstrip())
if path_result.success and path_result.library_path:
@@ -594,6 +595,9 @@ class QtDriver(DriverMixin, QObject):
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
FfmpegMissingMessageBox().show()
if is_version_outdated(VERSION, TagStudioCore.get_most_recent_release_version()):
OutOfDateMessageBox().exec()
self.app.exec()
self.shutdown()
@@ -1107,8 +1111,8 @@ class QtDriver(DriverMixin, QObject):
def run_macro(self, name: MacroID, entry_id: int):
"""Run a specific Macro on an Entry given a Macro name."""
entry: Entry = self.lib.get_entry(entry_id)
full_path = self.lib.library_dir / entry.path
entry: Entry = unwrap(self.lib.get_entry(entry_id))
full_path = unwrap(self.lib.library_dir) / entry.path
source = "" if entry.path.parent == Path(".") else entry.path.parts[0].lower()
logger.info(

View File

@@ -350,6 +350,9 @@
"trash.dialog.title.singular": "Delete File",
"trash.name.generic": "Trash",
"trash.name.windows": "Recycle Bin",
"version_modal.title": "TagStudio Update Available",
"version_modal.description": "A new version of TagStudio is available! You can download the latest release from <a href=\"https://github.com/TagStudioDev/TagStudio/releases/latest\">Github</a>.",
"version_modal.status": "Installed Version: {installed_version}<br>Latest Release Version: {latest_release_version}",
"view.size.0": "Mini",
"view.size.1": "Small",
"view.size.2": "Medium",