mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Inform the user for new updates
Add a hamburger button in the main window of Dangerzone, that will be the entry point for update information. Whenever a new update is released, users will see a green notification bubble. If an update error happens, they will see a red notification bubble. In the hamburger menu, users have the option to enable or disable update checks. Depending on the update check status, users will see in a pop-up dialog more info about the new update or the error. Closes #189
This commit is contained in:
parent
58c5fc846a
commit
5b17f75047
3 changed files with 212 additions and 4 deletions
|
@ -12,6 +12,8 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
||||||
- Platform support: Alpha integration with Qubes OS ([issue #411](https://github.com/freedomofpress/dangerzone/issues/411))
|
- Platform support: Alpha integration with Qubes OS ([issue #411](https://github.com/freedomofpress/dangerzone/issues/411))
|
||||||
- Platform support: Debian Trixie (13)
|
- Platform support: Debian Trixie (13)
|
||||||
- Platform support: Ubuntu 23.04 (Lunar Lobster)
|
- Platform support: Ubuntu 23.04 (Lunar Lobster)
|
||||||
|
- Inform about new updates on MacOS/Windows platforms, by periodically checking
|
||||||
|
our GitHub releases page ([issue #189](https://github.com/freedomofpress/dangerzone/issues/189))
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,9 @@ from ..isolation_provider.qubes import Qubes
|
||||||
from ..util import get_resource_path, get_version
|
from ..util import get_resource_path, get_version
|
||||||
from .logic import DangerzoneGui
|
from .logic import DangerzoneGui
|
||||||
from .main_window import MainWindow
|
from .main_window import MainWindow
|
||||||
|
from .updater import UpdaterThread
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Application(QtWidgets.QApplication):
|
class Application(QtWidgets.QApplication):
|
||||||
|
@ -117,6 +120,19 @@ def gui_main(
|
||||||
window.content_widget.doc_selection_widget.documents_selected.emit(documents)
|
window.content_widget.doc_selection_widget.documents_selected.emit(documents)
|
||||||
|
|
||||||
window = MainWindow(dangerzone)
|
window = MainWindow(dangerzone)
|
||||||
|
|
||||||
|
# Check for updates
|
||||||
|
log.debug("Setting up Dangezone updater")
|
||||||
|
updater = UpdaterThread(dangerzone)
|
||||||
|
window.register_update_handler(updater.finished)
|
||||||
|
|
||||||
|
log.debug("Consulting updater settings before checking for updates")
|
||||||
|
if updater.should_check_for_updates():
|
||||||
|
log.debug("Checking for updates")
|
||||||
|
updater.start()
|
||||||
|
else:
|
||||||
|
log.debug("Will not check for updates, based on updater settings")
|
||||||
|
|
||||||
if filenames:
|
if filenames:
|
||||||
open_files(filenames)
|
open_files(filenames)
|
||||||
|
|
||||||
|
|
|
@ -13,12 +13,12 @@ from colorama import Fore, Style
|
||||||
|
|
||||||
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from PySide2 import QtCore, QtGui, QtWidgets
|
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from PySide6 import QtCore, QtGui, QtWidgets
|
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from PySide2 import QtCore, QtGui, QtWidgets
|
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||||
|
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from ..document import SAFE_EXTENSION, Document
|
from ..document import SAFE_EXTENSION, Document
|
||||||
|
@ -26,15 +26,42 @@ from ..isolation_provider.container import Container, NoContainerTechException
|
||||||
from ..isolation_provider.dummy import Dummy
|
from ..isolation_provider.dummy import Dummy
|
||||||
from ..isolation_provider.qubes import Qubes
|
from ..isolation_provider.qubes import Qubes
|
||||||
from ..util import get_resource_path, get_subprocess_startupinfo, get_version
|
from ..util import get_resource_path, get_subprocess_startupinfo, get_version
|
||||||
from .logic import Alert, DangerzoneGui
|
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
||||||
|
from .updater import UpdateReport
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE_SUCCESS_MSG_INTRO = """\
|
||||||
|
<p>A new Dangerzone version has been released.</p>
|
||||||
|
<p>Please visit our <a href="https://dangerzone.rocks#downloads">downloads page</a> to install this
|
||||||
|
update.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE_ERROR_MSG_INTRO = """\
|
||||||
|
<p>Something went wrong while checking for Dangerzone updates:</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE_ERROR_MSG_OUTRO = """\
|
||||||
|
<p>You are strongly advised to visit our
|
||||||
|
<a href="https://dangerzone.rocks#downloads">downloads page</a> and check for new
|
||||||
|
updates manually, or consult our
|
||||||
|
<a href=https://github.com/freedomofpress/dangerzone/wiki/Updates>wiki page</a> for
|
||||||
|
common causes of errors. Alternatively, you can uncheck the "Check for updates" option
|
||||||
|
in our menu, if you are in an air-gapped environment and have another way of learning
|
||||||
|
about updates.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
HAMBURGER_MENU_SIZE = 30
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow):
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
def __init__(self, dangerzone: DangerzoneGui) -> None:
|
def __init__(self, dangerzone: DangerzoneGui) -> None:
|
||||||
super(MainWindow, self).__init__()
|
super(MainWindow, self).__init__()
|
||||||
self.dangerzone = dangerzone
|
self.dangerzone = dangerzone
|
||||||
|
self.updater_error: Optional[str] = None
|
||||||
|
|
||||||
self.setWindowTitle("Dangerzone")
|
self.setWindowTitle("Dangerzone")
|
||||||
self.setWindowIcon(self.dangerzone.get_window_icon())
|
self.setWindowIcon(self.dangerzone.get_window_icon())
|
||||||
|
@ -59,13 +86,52 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
header_version_label.setProperty("class", "version")
|
header_version_label.setProperty("class", "version")
|
||||||
header_version_label.setAlignment(QtCore.Qt.AlignBottom)
|
header_version_label.setAlignment(QtCore.Qt.AlignBottom)
|
||||||
|
|
||||||
|
# Create the hamburger button, whose main purpose is to inform the user about
|
||||||
|
# updates.
|
||||||
|
self.hamburger_button = QtWidgets.QToolButton()
|
||||||
|
self.hamburger_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
|
||||||
|
self.hamburger_button.setIcon(
|
||||||
|
QtGui.QIcon(self.load_svg_image("hamburger_menu.svg"))
|
||||||
|
)
|
||||||
|
self.hamburger_button.setFixedSize(HAMBURGER_MENU_SIZE, HAMBURGER_MENU_SIZE)
|
||||||
|
self.hamburger_button.setIconSize(
|
||||||
|
QtCore.QSize(HAMBURGER_MENU_SIZE, HAMBURGER_MENU_SIZE)
|
||||||
|
)
|
||||||
|
# FIXME: Maybe remove the box around the icon as well
|
||||||
|
self.hamburger_button.setStyleSheet(
|
||||||
|
"QToolButton::menu-indicator { image: none; }"
|
||||||
|
)
|
||||||
|
self.hamburger_button.setArrowType(QtCore.Qt.ArrowType.NoArrow)
|
||||||
|
|
||||||
|
# Create the menu for the hamburger button
|
||||||
|
hamburger_menu = QtWidgets.QMenu(self.hamburger_button)
|
||||||
|
self.hamburger_button.setMenu(hamburger_menu)
|
||||||
|
|
||||||
|
# Add the "Check for updates" action
|
||||||
|
self.toggle_updates_action = hamburger_menu.addAction("Check for updates")
|
||||||
|
self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered)
|
||||||
|
self.toggle_updates_action.setCheckable(True)
|
||||||
|
self.toggle_updates_action.setChecked(
|
||||||
|
bool(self.dangerzone.settings.get("updater_check"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the "Exit" action
|
||||||
|
hamburger_menu.addSeparator()
|
||||||
|
exit_action = hamburger_menu.addAction("Exit")
|
||||||
|
exit_action.triggered.connect(self.close)
|
||||||
|
|
||||||
header_layout = QtWidgets.QHBoxLayout()
|
header_layout = QtWidgets.QHBoxLayout()
|
||||||
|
header_layout.addSpacing(
|
||||||
|
HAMBURGER_MENU_SIZE
|
||||||
|
) # balance out hamburger to keep logo centered
|
||||||
header_layout.addStretch()
|
header_layout.addStretch()
|
||||||
header_layout.addWidget(logo)
|
header_layout.addWidget(logo)
|
||||||
header_layout.addSpacing(10)
|
header_layout.addSpacing(10)
|
||||||
header_layout.addWidget(header_label)
|
header_layout.addWidget(header_label)
|
||||||
header_layout.addWidget(header_version_label)
|
header_layout.addWidget(header_version_label)
|
||||||
header_layout.addStretch()
|
header_layout.addStretch()
|
||||||
|
header_layout.addWidget(self.hamburger_button)
|
||||||
|
header_layout.addSpacing(15)
|
||||||
|
|
||||||
if isinstance(self.dangerzone.isolation_provider, Container):
|
if isinstance(self.dangerzone.isolation_provider, Container):
|
||||||
# Waiting widget replaces content widget while container runtime isn't available
|
# Waiting widget replaces content widget while container runtime isn't available
|
||||||
|
@ -102,6 +168,130 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
def load_svg_image(self, filename: str) -> QtGui.QPixmap:
|
||||||
|
"""Load an SVG image from a filename.
|
||||||
|
|
||||||
|
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
||||||
|
"""
|
||||||
|
path = get_resource_path(filename)
|
||||||
|
svg_renderer = QtSvg.QSvgRenderer(path)
|
||||||
|
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||||
|
# Set the ARGB to 0 to prevent rendering artifacts
|
||||||
|
image.fill(0x00000000)
|
||||||
|
svg_renderer.render(QtGui.QPainter(image))
|
||||||
|
pixmap = QtGui.QPixmap.fromImage(image)
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
def show_update_success(self) -> None:
|
||||||
|
"""Inform the user about a new Dangerzone release."""
|
||||||
|
version = self.dangerzone.settings.get("updater_latest_version")
|
||||||
|
changelog = self.dangerzone.settings.get("updater_latest_changelog")
|
||||||
|
|
||||||
|
changelog_widget = CollapsibleBox("Changelog")
|
||||||
|
changelog_layout = QtWidgets.QVBoxLayout()
|
||||||
|
changelog_text_box = QtWidgets.QTextBrowser()
|
||||||
|
changelog_text_box.setHtml(changelog)
|
||||||
|
changelog_text_box.setOpenExternalLinks(True)
|
||||||
|
changelog_layout.addWidget(changelog_text_box)
|
||||||
|
changelog_widget.setContentLayout(changelog_layout)
|
||||||
|
|
||||||
|
update_widget = UpdateDialog(
|
||||||
|
self.dangerzone,
|
||||||
|
title=f"Dangerzone {version} has been released",
|
||||||
|
intro_msg=UPDATE_SUCCESS_MSG_INTRO,
|
||||||
|
middle_widget=changelog_widget,
|
||||||
|
epilogue_msg=None,
|
||||||
|
ok_text="Ok",
|
||||||
|
has_cancel=False,
|
||||||
|
)
|
||||||
|
update_widget.exec_()
|
||||||
|
|
||||||
|
def show_update_error(self) -> None:
|
||||||
|
"""Inform the user about an error during update checks"""
|
||||||
|
assert self.updater_error is not None
|
||||||
|
|
||||||
|
error_widget = QtWidgets.QTextBrowser()
|
||||||
|
error_widget.setHtml(self.updater_error)
|
||||||
|
|
||||||
|
update_widget = UpdateDialog(
|
||||||
|
self.dangerzone,
|
||||||
|
title="Update check error",
|
||||||
|
intro_msg=UPDATE_ERROR_MSG_INTRO,
|
||||||
|
middle_widget=error_widget,
|
||||||
|
epilogue_msg=UPDATE_ERROR_MSG_OUTRO,
|
||||||
|
ok_text="Close",
|
||||||
|
has_cancel=False,
|
||||||
|
)
|
||||||
|
update_widget.exec_()
|
||||||
|
|
||||||
|
def toggle_updates_triggered(self) -> None:
|
||||||
|
"""Change the underlying update check settings based on the user's choice."""
|
||||||
|
check = self.toggle_updates_action.isChecked()
|
||||||
|
self.dangerzone.settings.set("updater_check", check)
|
||||||
|
self.dangerzone.settings.save()
|
||||||
|
|
||||||
|
def handle_updates(self, report: UpdateReport) -> None:
|
||||||
|
"""Handle update reports from the update checker thread.
|
||||||
|
|
||||||
|
See Updater.check_for_updates() to find the different types of reports that it
|
||||||
|
may send back, depending on the outcome of an update check.
|
||||||
|
"""
|
||||||
|
# If there are no new updates, reset the error counter (if any) and return.
|
||||||
|
if report.empty():
|
||||||
|
self.dangerzone.settings.set("updater_errors", 0, autosave=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
hamburger_menu = self.hamburger_button.menu()
|
||||||
|
|
||||||
|
if report.error:
|
||||||
|
log.error(f"Encountered an error during an update check: {report.error}")
|
||||||
|
errors = self.dangerzone.settings.get("updater_errors") + 1
|
||||||
|
self.dangerzone.settings.set("updater_errors", errors)
|
||||||
|
self.dangerzone.settings.save()
|
||||||
|
self.updater_error = report.error
|
||||||
|
|
||||||
|
# If we encounter more than three errors in a row, show a red notification
|
||||||
|
# bubble. This way, we don't inform the user about intermittent errors.
|
||||||
|
if errors < 3:
|
||||||
|
log.debug(
|
||||||
|
f"Will not show an error yet since number of errors is low ({errors})"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.hamburger_button.setIcon(
|
||||||
|
QtGui.QIcon(self.load_svg_image("hamburger_menu_update_error.svg"))
|
||||||
|
)
|
||||||
|
sep = hamburger_menu.insertSeparator(hamburger_menu.actions()[0])
|
||||||
|
# FIXME: Add red bubble next to the text.
|
||||||
|
error_action = QtGui.QAction("Update error", hamburger_menu) # type: ignore [attr-defined]
|
||||||
|
error_action.triggered.connect(self.show_update_error)
|
||||||
|
hamburger_menu.insertAction(sep, error_action)
|
||||||
|
else:
|
||||||
|
log.debug(f"Handling new version: {report.version}")
|
||||||
|
self.dangerzone.settings.set("updater_latest_version", report.version)
|
||||||
|
self.dangerzone.settings.set("updater_latest_changelog", report.changelog)
|
||||||
|
self.dangerzone.settings.set("updater_errors", 0)
|
||||||
|
|
||||||
|
# FIXME: Save the settings to the filesystem only when they have really changed,
|
||||||
|
# maybe with a dirty bit.
|
||||||
|
self.dangerzone.settings.save()
|
||||||
|
|
||||||
|
self.hamburger_button.setIcon(
|
||||||
|
QtGui.QIcon(self.load_svg_image("hamburger_menu_update_success.svg"))
|
||||||
|
)
|
||||||
|
|
||||||
|
sep = hamburger_menu.insertSeparator(hamburger_menu.actions()[0])
|
||||||
|
# FIXME: Add green bubble next to the text.
|
||||||
|
success_action = QtGui.QAction("New version available", hamburger_menu) # type: ignore [attr-defined]
|
||||||
|
success_action.setIcon(
|
||||||
|
QtGui.QIcon(self.load_svg_image("hamburger_menu_update_available.svg"))
|
||||||
|
)
|
||||||
|
success_action.triggered.connect(self.show_update_success)
|
||||||
|
hamburger_menu.insertAction(sep, success_action)
|
||||||
|
|
||||||
|
def register_update_handler(self, signal: QtCore.SignalInstance) -> None:
|
||||||
|
signal.connect(self.handle_updates)
|
||||||
|
|
||||||
def waiting_finished(self) -> None:
|
def waiting_finished(self) -> None:
|
||||||
self.dangerzone.is_waiting_finished = True
|
self.dangerzone.is_waiting_finished = True
|
||||||
self.waiting_widget.hide()
|
self.waiting_widget.hide()
|
||||||
|
|
Loading…
Reference in a new issue