fix(updates): follow the build channel and allow turning update checks off (#999)
Use the running build channel for background checks and add a preference to disable them. Fixes #977
This commit is contained in:
@@ -239,8 +239,8 @@ DEFAULT_CONFIG = {
|
||||
"whispercpp_gpu_device": None, # None = auto-select discrete GPU; int = specific device index
|
||||
},
|
||||
"updates": {
|
||||
# "stable" follows GitHub /releases/latest; "nightly" follows nightly-YYYY-MM-DD tags.
|
||||
"channel": "stable",
|
||||
# Background checks control the tray entry and desktop notification.
|
||||
"check_automatically": True,
|
||||
# Tag last announced via desktop notification (avoids re-notifying every 6h).
|
||||
"last_notified_version": "",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,11 @@ import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa: E402
|
||||
|
||||
from ..utils.update_checker import is_trusted_release_url, normalize_channel
|
||||
from ..utils.update_checker import (
|
||||
DEFAULT_CHECK_AUTOMATICALLY,
|
||||
build_channel,
|
||||
is_trusted_release_url,
|
||||
)
|
||||
from ..version import __copyright__, __url__, __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -283,6 +287,7 @@ class AboutSectionMixin(Gtk.Dialog):
|
||||
about_tab = getattr(self, "about_tab")
|
||||
config_manager = getattr(self, "config_manager")
|
||||
on_check_updates_clicked = getattr(self, "_on_check_updates_clicked")
|
||||
on_update_checks_toggled = getattr(self, "_on_update_checks_toggled")
|
||||
on_update_channel_changed = getattr(self, "_on_update_channel_changed")
|
||||
on_open_release_clicked = getattr(self, "_on_open_release_clicked")
|
||||
|
||||
@@ -291,7 +296,6 @@ class AboutSectionMixin(Gtk.Dialog):
|
||||
from ..utils.resource_manager import ResourceManager
|
||||
from .settings_dialog import (
|
||||
_CONTROL_WIDTH,
|
||||
DEFAULT_UPDATE_CHANNEL,
|
||||
PreferenceRow,
|
||||
PreferencesGroup,
|
||||
_combo_chrome,
|
||||
@@ -427,12 +431,33 @@ class AboutSectionMixin(Gtk.Dialog):
|
||||
)
|
||||
about_tab.pack_start(talk_group, False, False, 0)
|
||||
|
||||
build = build_channel()
|
||||
updates_group = PreferencesGroup(
|
||||
title="Updates",
|
||||
description="Stable for numbered releases. Nightly for dated development builds.",
|
||||
description=(
|
||||
f"This is a {build} build, so background checks only look for "
|
||||
f"{build} releases. Pick a channel below to look one up by hand."
|
||||
),
|
||||
keywords=("update", "version", "release", "upgrade", "appimage", "nightly", "channel"),
|
||||
)
|
||||
|
||||
self.update_checks_switch = Gtk.Switch()
|
||||
self.update_checks_switch.set_active(
|
||||
config_manager.get_bool("updates", "check_automatically", DEFAULT_CHECK_AUTOMATICALLY)
|
||||
)
|
||||
self.update_checks_switch.connect("state-set", on_update_checks_toggled)
|
||||
updates_group.add_row(
|
||||
PreferenceRow(
|
||||
title="Check for updates automatically",
|
||||
subtitle=(
|
||||
"Look for new releases in the background and show a tray entry and a "
|
||||
"notification when one is out"
|
||||
),
|
||||
widget=self.update_checks_switch,
|
||||
keywords=("update", "notification", "automatic", "check", "disable"),
|
||||
)
|
||||
)
|
||||
|
||||
self.update_channel_combo = Gtk.ComboBoxText()
|
||||
self.update_channel_combo.append("stable", "Stable")
|
||||
self.update_channel_combo.append("nightly", "Nightly")
|
||||
@@ -443,10 +468,7 @@ class AboutSectionMixin(Gtk.Dialog):
|
||||
"nightly-YYYY-MM-DD build."
|
||||
),
|
||||
)
|
||||
saved_channel = normalize_channel(
|
||||
config_manager.get_str("updates", "channel", DEFAULT_UPDATE_CHANNEL)
|
||||
)
|
||||
self.update_channel_combo.set_active_id(saved_channel)
|
||||
self.update_channel_combo.set_active_id(build_channel())
|
||||
|
||||
self.check_updates_btn = Gtk.Button(label="Check")
|
||||
self.check_updates_btn.get_style_context().add_class("suffix-button")
|
||||
|
||||
@@ -74,8 +74,10 @@ from ..utils.system_language import ( # noqa: E402
|
||||
language_for_active_layout,
|
||||
)
|
||||
from ..utils.update_checker import ( # noqa: E402
|
||||
DEFAULT_CHECK_AUTOMATICALLY,
|
||||
DEFAULT_UPDATE_CHANNEL,
|
||||
ReleaseInfo,
|
||||
build_channel,
|
||||
fetch_latest_release,
|
||||
format_release_notes,
|
||||
is_trusted_release_url,
|
||||
@@ -2452,12 +2454,13 @@ class SettingsDialog(AboutSectionMixin):
|
||||
initial_page: Optional[str] = None,
|
||||
pending_update: Optional[ReleaseInfo] = None,
|
||||
update_status_callback: Optional[Callable[..., Any]] = None,
|
||||
update_checks_callback: Optional[Callable[[bool], None]] = None,
|
||||
overlay_enabled_callback: Optional[Callable[[bool], None]] = None,
|
||||
hotkey_listener_update_callback: Optional[Callable[[], None]] = None,
|
||||
language_shortcuts_update_callback: Optional[Callable[[], None]] = None,
|
||||
history_update_callback: Optional[Callable[[], None]] = None,
|
||||
history_clear_callback: Optional[Callable[[], None]] = None,
|
||||
):
|
||||
) -> None:
|
||||
super().__init__(title="Vocalinux Settings", transient_for=parent, flags=0)
|
||||
# Force window decorations (title-bar close) on all WMs. An in-window
|
||||
# Close button also lives in the sidebar footer so the dialog always
|
||||
@@ -2470,6 +2473,7 @@ class SettingsDialog(AboutSectionMixin):
|
||||
self.dictionary_manager = getattr(speech_engine, "dictionary_manager", None)
|
||||
self.shortcut_update_callback = shortcut_update_callback
|
||||
self.update_status_callback = update_status_callback
|
||||
self.update_checks_callback = update_checks_callback
|
||||
self.overlay_enabled_callback = overlay_enabled_callback
|
||||
self.hotkey_listener_update_callback = hotkey_listener_update_callback
|
||||
self.language_shortcuts_update_callback = language_shortcuts_update_callback
|
||||
@@ -2836,14 +2840,21 @@ class SettingsDialog(AboutSectionMixin):
|
||||
if row is not None:
|
||||
self.settings_stack.set_visible_child_name(row.page_name)
|
||||
|
||||
def _on_settings_page_changed(self, stack, pspec):
|
||||
def _on_settings_page_changed(self, stack: Gtk.Stack, pspec: Any) -> None:
|
||||
"""Persist deferred edits when the visible settings page changes."""
|
||||
visible = stack.get_visible_child_name()
|
||||
if visible != "advanced":
|
||||
self._flush_advanced_prompt_if_dirty()
|
||||
if visible == "about" and not self._update_auto_checked:
|
||||
self._update_auto_checked = True
|
||||
self._start_update_check()
|
||||
if self.config_manager.get_bool(
|
||||
"updates", "check_automatically", DEFAULT_CHECK_AUTOMATICALLY
|
||||
):
|
||||
self._start_update_check()
|
||||
else:
|
||||
self.update_status_row.set_subtitle(
|
||||
"Automatic checks are off. Press Check to look now."
|
||||
)
|
||||
|
||||
def _open_web_url(self, url: str) -> None:
|
||||
"""Open a trusted project URL in the user's default browser."""
|
||||
@@ -6345,13 +6356,25 @@ class SettingsDialog(AboutSectionMixin):
|
||||
channel_id = self.update_channel_combo.get_active_id()
|
||||
return normalize_channel(channel_id or DEFAULT_UPDATE_CHANNEL)
|
||||
|
||||
def _on_update_channel_changed(self, widget):
|
||||
"""Persist channel choice and refresh the update check."""
|
||||
def _on_update_checks_toggled(self, widget: Gtk.Switch, state: bool) -> bool:
|
||||
"""Persist automatic update-check preference and notify the tray."""
|
||||
if _handlers_suppressed(self):
|
||||
return False
|
||||
|
||||
enabled = bool(state)
|
||||
self.config_manager.set("updates", "check_automatically", enabled)
|
||||
self.config_manager.save_settings()
|
||||
if self.update_checks_callback is not None:
|
||||
try:
|
||||
self.update_checks_callback(enabled)
|
||||
except Exception:
|
||||
logger.error("Automatic update checks callback failed", exc_info=True)
|
||||
return False
|
||||
|
||||
def _on_update_channel_changed(self, widget: Gtk.ComboBoxText) -> None:
|
||||
"""Refresh the manual update lookup for the selected channel."""
|
||||
if _handlers_suppressed(self):
|
||||
return
|
||||
channel = self._current_update_channel()
|
||||
self.config_manager.set("updates", "channel", channel)
|
||||
self.config_manager.save_settings()
|
||||
self._start_update_check()
|
||||
|
||||
def _on_check_updates_clicked(self, widget):
|
||||
@@ -6399,10 +6422,10 @@ class SettingsDialog(AboutSectionMixin):
|
||||
|
||||
def _apply_update_check_result(
|
||||
self,
|
||||
release,
|
||||
release: Optional[ReleaseInfo],
|
||||
channel: Optional[str] = None,
|
||||
generation: Optional[int] = None,
|
||||
):
|
||||
) -> bool:
|
||||
"""Update About-page UI from a release lookup result."""
|
||||
if not self._dialog_is_alive():
|
||||
return False
|
||||
@@ -6467,7 +6490,13 @@ class SettingsDialog(AboutSectionMixin):
|
||||
self._set_about_update_badge(False)
|
||||
|
||||
# Keep the tray menu in sync with an About-page check (no extra notification).
|
||||
if self.update_status_callback is not None:
|
||||
if (
|
||||
channel == build_channel()
|
||||
and self.config_manager.get_bool(
|
||||
"updates", "check_automatically", DEFAULT_CHECK_AUTOMATICALLY
|
||||
)
|
||||
and self.update_status_callback is not None
|
||||
):
|
||||
try:
|
||||
self.update_status_callback(update_available, release if update_available else None)
|
||||
except Exception:
|
||||
|
||||
@@ -49,7 +49,7 @@ from ..speech_recognition.diarization import (
|
||||
from ..suspend_handler import SuspendHandler
|
||||
from ..utils.host_process import host_env
|
||||
from ..utils.resource_manager import ResourceManager
|
||||
from ..utils.update_checker import ReleaseInfo
|
||||
from ..utils.update_checker import DEFAULT_CHECK_AUTOMATICALLY, ReleaseInfo
|
||||
from ..utils.update_monitor import UpdateMonitor
|
||||
from ..utils.whispercpp_model_info import TDRZ_MODEL, WHISPERCPP_MODEL_INFO, is_model_downloaded
|
||||
from . import notifications
|
||||
@@ -288,7 +288,7 @@ class TrayIndicator:
|
||||
|
||||
# Background GitHub update checks (tray menu + optional notification)
|
||||
self._update_monitor = UpdateMonitor(
|
||||
get_channel=self._get_update_channel,
|
||||
get_enabled=self._update_checks_enabled,
|
||||
on_result=self._on_update_check_result,
|
||||
)
|
||||
self._update_monitor.start()
|
||||
@@ -1343,6 +1343,7 @@ class TrayIndicator:
|
||||
update_status_callback=lambda available, release: self._apply_update_status(
|
||||
available, release, notify=False
|
||||
),
|
||||
update_checks_callback=self._on_update_checks_toggled,
|
||||
overlay_enabled_callback=self.set_overlay_enabled,
|
||||
hotkey_listener_update_callback=self._setup_keyboard_shortcuts,
|
||||
language_shortcuts_update_callback=self.refresh_language_shortcuts,
|
||||
@@ -1364,9 +1365,18 @@ class TrayIndicator:
|
||||
if self._settings_dialog is dialog:
|
||||
self._settings_dialog = None
|
||||
|
||||
def _get_update_channel(self) -> str:
|
||||
"""Return the configured release channel for background update checks."""
|
||||
return self.config_manager.get_str("updates", "channel", "stable")
|
||||
def _update_checks_enabled(self) -> bool:
|
||||
"""Return whether background update checks are enabled."""
|
||||
return self.config_manager.get_bool(
|
||||
"updates", "check_automatically", DEFAULT_CHECK_AUTOMATICALLY
|
||||
)
|
||||
|
||||
def _on_update_checks_toggled(self, enabled: bool) -> None:
|
||||
"""Update tray state when automatic update checks change."""
|
||||
if not enabled:
|
||||
self._apply_update_status(False, None, notify=False)
|
||||
else:
|
||||
self._update_monitor.check_now()
|
||||
|
||||
def _on_update_check_result(self, available: bool, release: Optional[ReleaseInfo]):
|
||||
"""Handle a background update-check result (already on the GLib main loop)."""
|
||||
@@ -1403,6 +1413,8 @@ class TrayIndicator:
|
||||
|
||||
def _maybe_notify_update(self, release: ReleaseInfo) -> None:
|
||||
"""Send one desktop notification per new version when notifications are on."""
|
||||
if not self._update_checks_enabled():
|
||||
return
|
||||
if not self.config_manager.get_bool("ui", "show_notifications", True):
|
||||
return
|
||||
if self.config_manager.get_str("updates", "last_notified_version", "") == release.tag_name:
|
||||
|
||||
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import version as _version_module
|
||||
from ..version import __url__
|
||||
from .proxy import requests_proxies
|
||||
|
||||
@@ -13,6 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_CHANNELS = ("stable", "nightly")
|
||||
DEFAULT_UPDATE_CHANNEL = "stable"
|
||||
DEFAULT_CHECK_AUTOMATICALLY = True
|
||||
|
||||
_GITHUB_REPO_RE = re.compile(r"https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?/?$")
|
||||
_PRE_RELEASE_SUFFIX_RE = re.compile(
|
||||
@@ -111,6 +113,13 @@ def _nightly_date(tag_or_version: str) -> Optional[str]:
|
||||
return f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}"
|
||||
|
||||
|
||||
def build_channel(version: Optional[str] = None) -> str:
|
||||
"""Return the channel for this build; nightly builds carry .devYYYYMMDD from CI."""
|
||||
if version is None:
|
||||
version = _version_module.__version__
|
||||
return "nightly" if _nightly_date(version) is not None else "stable"
|
||||
|
||||
|
||||
def is_update_available(
|
||||
current: str, latest: ReleaseInfo, channel: str = DEFAULT_UPDATE_CHANNEL
|
||||
) -> bool:
|
||||
|
||||
@@ -12,8 +12,10 @@ from typing import Callable, Optional
|
||||
|
||||
from ..version import __version__
|
||||
from .update_checker import (
|
||||
DEFAULT_CHECK_AUTOMATICALLY,
|
||||
DEFAULT_UPDATE_CHANNEL,
|
||||
ReleaseInfo,
|
||||
build_channel,
|
||||
fetch_latest_release,
|
||||
is_update_available,
|
||||
normalize_channel,
|
||||
@@ -31,8 +33,11 @@ class UpdateMonitor:
|
||||
|
||||
Args:
|
||||
get_channel: Returns ``stable`` / ``nightly`` (re-read each check).
|
||||
When None, the running build determines the channel.
|
||||
on_result: ``on_result(available, release)`` on the GLib main loop.
|
||||
``release`` is set only when ``available`` is True.
|
||||
get_enabled: Returns whether automatic update checks are enabled.
|
||||
When None, automatic checks are always enabled.
|
||||
startup_delay_seconds: Delay before the first check.
|
||||
check_interval_seconds: Delay between subsequent checks.
|
||||
use_glib: Schedule via GLib (False = tests drive ``_run_check``).
|
||||
@@ -40,14 +45,16 @@ class UpdateMonitor:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
get_channel: Callable[[], str],
|
||||
get_channel: Optional[Callable[[], str]] = None,
|
||||
on_result: Optional[Callable[[bool, Optional[ReleaseInfo]], None]] = None,
|
||||
get_enabled: Optional[Callable[[], bool]] = None,
|
||||
startup_delay_seconds: float = DEFAULT_STARTUP_DELAY_SECONDS,
|
||||
check_interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
|
||||
use_glib: bool = True,
|
||||
):
|
||||
) -> None:
|
||||
self._get_channel = get_channel
|
||||
self._on_result = on_result
|
||||
self._get_enabled = get_enabled
|
||||
self._startup_delay = max(0.0, float(startup_delay_seconds))
|
||||
self._check_interval = max(60.0, float(check_interval_seconds))
|
||||
self._use_glib = use_glib
|
||||
@@ -103,7 +110,7 @@ class UpdateMonitor:
|
||||
pass
|
||||
self._timeout_id = None
|
||||
|
||||
def _on_timer(self):
|
||||
def _on_timer(self) -> bool:
|
||||
self._timeout_id = None
|
||||
if self._running:
|
||||
self._run_check()
|
||||
@@ -112,6 +119,11 @@ class UpdateMonitor:
|
||||
def _run_check(self) -> None:
|
||||
if self._check_in_progress:
|
||||
return
|
||||
if not self._is_enabled():
|
||||
logger.debug("Automatic update checks are off")
|
||||
if self._running:
|
||||
self._schedule(self._check_interval)
|
||||
return
|
||||
self._check_in_progress = True
|
||||
self._generation += 1
|
||||
generation = self._generation
|
||||
@@ -145,13 +157,32 @@ class UpdateMonitor:
|
||||
)
|
||||
|
||||
def _current_channel(self) -> str:
|
||||
"""Read and normalize the live channel preference."""
|
||||
"""Read the live channel preference or infer the running build's channel."""
|
||||
try:
|
||||
if self._get_channel is None:
|
||||
return build_channel(__version__)
|
||||
return normalize_channel(self._get_channel())
|
||||
except Exception:
|
||||
logger.error("Failed to read update channel", exc_info=True)
|
||||
return DEFAULT_UPDATE_CHANNEL
|
||||
|
||||
def _is_enabled(self) -> bool:
|
||||
"""Return whether automatic update checks are enabled."""
|
||||
if self._get_enabled is None:
|
||||
return True
|
||||
try:
|
||||
return bool(self._get_enabled())
|
||||
except Exception:
|
||||
logger.error("Failed to read automatic update setting", exc_info=True)
|
||||
return DEFAULT_CHECK_AUTOMATICALLY
|
||||
|
||||
def check_now(self) -> None:
|
||||
"""Run an update check now when the monitor is active and enabled."""
|
||||
if not self._running or self._check_in_progress or not self._is_enabled():
|
||||
return
|
||||
self._cancel_timeout()
|
||||
self._run_check()
|
||||
|
||||
def _finish_check(
|
||||
self,
|
||||
generation: int,
|
||||
@@ -165,6 +196,11 @@ class UpdateMonitor:
|
||||
self._check_in_progress = False
|
||||
if not self._running or generation != self._generation:
|
||||
return False
|
||||
if callback and not self._is_enabled():
|
||||
logger.debug("Automatic update checks are off")
|
||||
if self._running:
|
||||
self._schedule(self._check_interval)
|
||||
return False
|
||||
# Drop results from an earlier channel (About-page checks do the same).
|
||||
if callback:
|
||||
current_channel = self._current_channel()
|
||||
|
||||
Reference in New Issue
Block a user