mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
39
src/tagstudio/qt/controllers/out_of_date_message_box.py
Normal file
39
src/tagstudio/qt/controllers/out_of_date_message_box.py
Normal 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}")
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user