From f9f47d1429a532e4e62d9f8188857179043be3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?= Date: Tue, 25 Jun 2019 18:25:04 -0300 Subject: [PATCH] Downgrade --- CHANGELOG.md | 3 +- README.md | 1 + fpakman/app.py | 3 +- fpakman/core/flatpak.py | 45 ++++++++++++++--- fpakman/core/model.py | 24 +++++++-- fpakman/core/util.py | 8 ++- fpakman/resources/img/downgrade.svg | 72 +++++++++++++++++++++++++++ fpakman/resources/locale/en | 12 +++++ fpakman/resources/locale/es | 14 +++++- fpakman/resources/locale/pt | 12 +++++ fpakman/view/qt/apps_table.py | 36 +++++++++++--- fpakman/view/qt/dialog.py | 31 ++++++++++++ fpakman/view/qt/root.py | 39 +++++++++++++++ fpakman/view/qt/systray.py | 6 ++- fpakman/view/qt/window.py | 75 ++++++++++++++++++++++++++--- 15 files changed, 349 insertions(+), 32 deletions(-) create mode 100755 fpakman/resources/img/downgrade.svg create mode 100644 fpakman/view/qt/dialog.py create mode 100644 fpakman/view/qt/root.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fe3c08..d224c420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.3.0] ### Features -- Allows app uninstall by right clicking it. +- Allows app uninstall and downgrade by right clicking it. ### Improvements - Adding tooltips to toolbar buttons - "Update ?" column renamed to "Upgrade ?" - Management panel title renamed - Showing runtime apps when no app is available +- Allowing to specify a custom app translation with the environment variable FPAKMAN_LOCALE ## [0.2.1] - 2019-06-24 ### Features diff --git a/README.md b/README.md index c3a78d5b..99b65781 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ In order to autostart the application, use your Desktop Environment settings to You can change some application settings via environment variables: - **FPAKMAN_UPDATE_NOTIFICATION**: enable or disable system updates notifications. Use **0** (disable) or **1** (enable, default). - **FPAKMAN_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60. +- **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale. ### Roadmap - Search and install applications diff --git a/fpakman/app.py b/fpakman/app.py index 0ee4c73c..8700fc90 100755 --- a/fpakman/app.py +++ b/fpakman/app.py @@ -16,7 +16,7 @@ parser = argparse.ArgumentParser(prog='fpakman', description="GUI for Flatpak ap parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) args = parser.parse_args() -locale_keys = util.get_locale_keys() +locale_keys = util.get_locale_keys(os.getenv('FPAKMAN_LOCALE', None)) app = QApplication(sys.argv) app.setWindowIcon(QIcon(resource.get_path('img/flathub_45.svg'))) @@ -26,6 +26,7 @@ controller = FlatpakController(manager) trayIcon = TrayIcon(locale_keys=locale_keys, controller=controller, + manager=manager, check_interval=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60))) trayIcon.show() diff --git a/fpakman/core/flatpak.py b/fpakman/core/flatpak.py index b42cdd8c..9933eea2 100755 --- a/fpakman/core/flatpak.py +++ b/fpakman/core/flatpak.py @@ -1,4 +1,5 @@ import re +import subprocess from typing import List from fpakman.core import system @@ -32,20 +33,33 @@ def app_str_to_json(line: str, version: str) -> dict: else: raise Exception('Unsupported version') - info = re.findall(r'\w+:\s.+', get_app_info(app['id'], app['branch'])) - fields_to_get = ['origin', 'arch', 'ref'] + extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True) + app.update(extra_fields) + + return app + + +def get_app_info_fields(app_id: str, branch: str, fields: List[str], check_runtime: bool = False): + info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch)) + data = {} + fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0) for field in info: + + if fields_to_retrieve == 0: + break + field_val = field.split(':') field_name = field_val[0].lower() - if field_name in fields_to_get: - app[field_name] = field_val[1].strip() + if field_name in fields or (check_runtime and field_name == 'ref'): + data[field_name] = field_val[1].strip() + fields_to_retrieve -= 1 - if field_name == 'ref': - app['runtime'] = app['ref'].startswith('runtime/') + if check_runtime and field_name == 'ref': + data['runtime'] = data['ref'].startswith('runtime/') - return app + return data def get_version(): @@ -92,3 +106,20 @@ def uninstall_and_stream(app_ref: str): def list_updates_as_str(): return system.run_cmd('flatpak update', ignore_return_code=True) + + +def downgrade_and_stream(app_ref: str, commit: str, root_password: str): + + pwdin, downgrade_cmd = None, [] + + if root_password is not None: + downgrade_cmd.extend(['sudo', '-S']) + pwdin = subprocess.Popen(['echo', root_password], stdout=subprocess.PIPE).stdout + + downgrade_cmd.extend(['flatpak', 'update', '--commit={}'.format(commit), app_ref, '-y']) + return subprocess.Popen(downgrade_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout + + +def get_app_commits(app_ref: str, origin: str) -> List[str]: + log = system.run_cmd('flatpak remote-info --log {} {}'.format(origin, app_ref)) + return re.findall(r'Commit+:\s(.+)', log) diff --git a/fpakman/core/model.py b/fpakman/core/model.py index 904b0116..7b953806 100755 --- a/fpakman/core/model.py +++ b/fpakman/core/model.py @@ -2,9 +2,6 @@ from threading import Lock, Thread from typing import List import requests -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from fpakman.core import flatpak @@ -12,6 +9,10 @@ __FLATHUB_URL__ = 'https://flathub.org' __FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' +class ImpossibleDowngradeException(Exception): + pass + + class FlatpakManager: def __init__(self): @@ -122,3 +123,20 @@ class FlatpakManager: if app_found: return flatpak.uninstall_and_stream(ref) + + def downgrade_app(self, ref: str, root_password: str): + + if self.apps: + + app_found = self._find_by_ref(ref) + + if app_found: + commits = flatpak.get_app_commits(app_found['ref'], app_found['origin']) + + commit_idx = commits.index(app_found['commit']) + + # downgrade is not possible if the app current commit in the first one: + if commit_idx == len(commits) - 1: + raise ImpossibleDowngradeException() + + return flatpak.downgrade_and_stream(app_found['ref'], commits[commit_idx + 1], root_password) diff --git a/fpakman/core/util.py b/fpakman/core/util.py index f66016ca..4a6afda1 100644 --- a/fpakman/core/util.py +++ b/fpakman/core/util.py @@ -4,11 +4,15 @@ from fpakman.core import resource import glob -def get_locale_keys(): +def get_locale_keys(key: str = None): - current_locale = locale.getdefaultlocale() locale_path = None + if key is None: + current_locale = locale.getdefaultlocale() + else: + current_locale = [key] + if current_locale: current_locale = current_locale[0] diff --git a/fpakman/resources/img/downgrade.svg b/fpakman/resources/img/downgrade.svg new file mode 100755 index 00000000..f1e6a7bd --- /dev/null +++ b/fpakman/resources/img/downgrade.svg @@ -0,0 +1,72 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/fpakman/resources/locale/en b/fpakman/resources/locale/en index d03599a8..3c1c96a5 100644 --- a/fpakman/resources/locale/en +++ b/fpakman/resources/locale/en @@ -10,15 +10,27 @@ manage_window.columns.update=Upgrade ? manage_window.apps_table.row.actions.uninstall=Uninstall manage_window.apps_table.row.actions.uninstall.popup.title=Uninstall manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} ? +manage_window.apps_table.row.actions.downgrade=Downgrade +manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ? manage_window.checkbox.only_apps=Apps manage_window.label.updates=Updates manage_window.status.refreshing=Refreshing manage_window.status.upgrading=Upgrading manage_window.status.uninstalling=Uninstalling +manage_window.status.downgrading=Downgrading manage_window.bt.refresh.tooltip=Reload the data about installed applications manage_window.bt.upgrade.tooltip=Upgrade all selected applications popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.msg=flatpak seems not to be installed. Exiting +popup.root.title=Requires root privileges +popup.root.password=Password +popup.root.bad_password.title=Error +popup.root.bad_password.body=Wrong password +popup.downgrade.impossible.title=Impossible to downgrade +popup.downgrade.impossible.body=The app is in its first version +popup.button.yes=Yes +popup.button.no=No +popup.button.cancel=Cancel tray.action.manage=Manage applications tray.action.exit=Exit notification.new_updates=Updates found \ No newline at end of file diff --git a/fpakman/resources/locale/es b/fpakman/resources/locale/es index 8b8f44a6..ad3da9ff 100644 --- a/fpakman/resources/locale/es +++ b/fpakman/resources/locale/es @@ -2,7 +2,7 @@ manage_window.title=Administrador de Aplicativos Flatpak manage_window.columns.name=Nombre manage_window.columns.version=Versión manage_window.columns.latest_version=Ultima Versión -manage_window.columns.branch=Branch +manage_window.columns.branch=Rama manage_window.columns.arch=Arch manage_window.columns.ref=Ref manage_window.columns.origin=Origen @@ -10,15 +10,27 @@ manage_window.columns.update=¿Actualizar? manage_window.apps_table.row.actions.uninstall=Desinstalar manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación manage_window.apps_table.row.actions.uninstall.popup.body=¿Eliminar {}? +manage_window.apps_table.row.actions.downgrade=Revertir versión +manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quieres revertir la versión actual de {}? manage_window.checkbox.only_apps=Apps manage_window.label.updates=Actualizaciones manage_window.status.refreshing=Recargando manage_window.status.upgrading=Actualizando manage_window.status.uninstalling=Desinstalando +manage_window.status.downgrading=Revirtiendo manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.msg=flatpak no parece estar instalado. Saliendo +popup.root.title=Requiere privilegios de root +popup.root.password=Contraseña +popup.root.bad_password.title=Error +popup.root.bad_password.body=Contraseña incorrecta +popup.downgrade.impossible.title=Imposible revertir la versión +popup.downgrade.impossible.body=El aplicativo está en su primera versión +popup.button.yes=Sí +popup.button.no=No +popup.button.cancel=Cancelar tray.action.manage=Administrar aplicativos tray.action.exit=Salir notification.new_updates=Actualizaciones encontradas \ No newline at end of file diff --git a/fpakman/resources/locale/pt b/fpakman/resources/locale/pt index 759936b4..d917c4d2 100644 --- a/fpakman/resources/locale/pt +++ b/fpakman/resources/locale/pt @@ -10,15 +10,27 @@ manage_window.columns.update=Atualizar ? manage_window.apps_table.row.actions.uninstall=Desinstalar manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação manage_window.apps_table.row.actions.uninstall.popup.body=Remover {}? +manage_window.apps_table.row.actions.downgrade=Reverter versão +manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ? manage_window.checkbox.only_apps=Apps manage_window.label.updates=Atualizações manage_window.status.refreshing=Recarregando manage_window.status.upgrading=Atualizando manage_window.status.uninstalling=Desinstalando +manage_window.status.downgrading=Revertendo manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados popup.flatpak_not_installed.title=Erro popup.flatpak_not_installed.msg=flatpak não parece estar instalado. Saindo +popup.root.title=Requer privilégios de root +popup.root.password=Senha +popup.root.bad_password.title=Erro +popup.root.bad_password.body=Senha incorreta +popup.downgrade.impossible.title=Impossível reverter a versão +popup.downgrade.impossible.body=O aplicativo está na sua primeira versão +popup.button.yes=Sim +popup.button.no=Não +popup.button.cancel=Cancelar tray.action.manage=Gerenciar aplicativos tray.action.exit=Sair notification.new_updates=Atualizações encontradas \ No newline at end of file diff --git a/fpakman/view/qt/apps_table.py b/fpakman/view/qt/apps_table.py index 4671b847..6478f64e 100644 --- a/fpakman/view/qt/apps_table.py +++ b/fpakman/view/qt/apps_table.py @@ -8,6 +8,7 @@ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidg from fpakman.core import resource from fpakman.core.controller import FlatpakController +from fpakman.view.qt import dialog class UpdateToggleButton(QToolButton): @@ -48,6 +49,7 @@ class AppsTable(QTableWidget): self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.icon_flathub = QIcon(resource.get_path('img/flathub_45.svg')) self.icon_uninstall = QIcon(resource.get_path('img/uninstall.svg')) + self.icon_downgrade = QIcon(resource.get_path('img/downgrade.svg')) self.network_man = QNetworkAccessManager() self.network_man.finished.connect(self._load_icon) @@ -60,21 +62,39 @@ class AppsTable(QTableWidget): action_uninstall = QAction(self.parent.locale_keys["manage_window.apps_table.row.actions.uninstall"]) action_uninstall.setIcon(self.icon_uninstall) action_uninstall.triggered.connect(self._uninstall_app) - menu_row.addAction(action_uninstall) + + app = self._get_selected_app() + + if not app['model']['runtime']: # downgrade only allowed for apps + action_downgrade = QAction(self.parent.locale_keys["manage_window.apps_table.row.actions.downgrade"]) + action_downgrade.triggered.connect(self._downgrade_app) + action_downgrade.setIcon(self.icon_downgrade) + menu_row.addAction(action_downgrade) + menu_row.adjustSize() menu_row.popup(QCursor.pos()) menu_row.exec_() - def _uninstall_app(self): - selected_app = self.parent.apps[self.currentRow()] + def _get_selected_app(self): + return self.parent.apps[self.currentRow()] - confirmation = QMessageBox.question(self, self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], - self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) - if confirmation == QMessageBox.Yes: + def _uninstall_app(self): + selected_app = self._get_selected_app() + + if dialog.ask_confirmation(title=self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], + body=self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), + locale_keys=self.parent.locale_keys): self.parent.uninstall_app(selected_app['model']['ref']) + def _downgrade_app(self): + selected_app = self._get_selected_app() + + if dialog.ask_confirmation(title=self.parent.locale_keys['manage_window.apps_table.row.actions.downgrade'], + body=self.parent.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']), + locale_keys=self.parent.locale_keys): + self.parent.downgrade_app(selected_app['model']['ref']) + def _load_icon(self, http_response): icon_url = http_response.url().toString() pixmap = QPixmap() @@ -122,7 +142,7 @@ class AppsTable(QTableWidget): col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.setItem(idx, 2, col_release) - if app['model']['update']: + if app['model']['version'] and app['model']['latest_version'] and app['model']['version'] < app['model']['latest_version']: col_release.setForeground(QColor('orange')) col_branch = QTableWidgetItem() diff --git a/fpakman/view/qt/dialog.py b/fpakman/view/qt/dialog.py new file mode 100644 index 00000000..ec9c8210 --- /dev/null +++ b/fpakman/view/qt/dialog.py @@ -0,0 +1,31 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMessageBox + +from fpakman.core import resource + + +def show_error(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/flathub_45.svg'))): + error_msg = QMessageBox() + error_msg.setIcon(QMessageBox.Critical) + error_msg.setWindowTitle(title) + error_msg.setText(body) + + if icon: + error_msg.setWindowIcon(icon) + error_msg.exec_() + + +def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIcon(resource.get_path('img/flathub_45.svg'))): + dialog_confirmation = QMessageBox() + dialog_confirmation.setIcon(QMessageBox.Critical) + dialog_confirmation.setWindowTitle(title) + dialog_confirmation.setText(body) + bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole) + dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole) + + if icon: + dialog_confirmation.setWindowIcon(icon) + + dialog_confirmation.exec_() + + return dialog_confirmation.clickedButton() == bt_yes diff --git a/fpakman/view/qt/root.py b/fpakman/view/qt/root.py new file mode 100644 index 00000000..0d68b3ea --- /dev/null +++ b/fpakman/view/qt/root.py @@ -0,0 +1,39 @@ +import os +import subprocess + +from PyQt5.QtWidgets import QInputDialog, QLineEdit + +from fpakman.view.qt.dialog import show_error + + +def is_root(): + return os.getuid() == 0 + + +def ask_root_password(locale_keys: dict): + + dialog_pwd = QInputDialog() + dialog_pwd.setInputMode(QInputDialog.TextInput) + dialog_pwd.setTextEchoMode(QLineEdit.Password) + dialog_pwd.setWindowTitle(locale_keys['popup.root.title']) + dialog_pwd.setLabelText(locale_keys['popup.root.password'] + ':') + dialog_pwd.setCancelButtonText(locale_keys['popup.button.cancel']) + dialog_pwd.resize(400, 200) + + ok = dialog_pwd.exec_() + + if ok: + if not validate_password(dialog_pwd.textValue()): + show_error(title=locale_keys['popup.root.bad_password.title'], + body=locale_keys['popup.root.bad_password.body']) + ok = False + + return dialog_pwd.textValue(), ok + + +def validate_password(password: str) -> bool: + f = os.popen('echo {} | sudo -S whoami'.format(password)) + res = f.read() + f.close() + + return bool(res.strip()) diff --git a/fpakman/view/qt/systray.py b/fpakman/view/qt/systray.py index c97a4834..00ff02ac 100755 --- a/fpakman/view/qt/systray.py +++ b/fpakman/view/qt/systray.py @@ -4,6 +4,7 @@ import time from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QSystemTrayIcon, QMenu +from fpakman.core.model import FlatpakManager from fpakman.core import resource from fpakman.core.controller import FlatpakController @@ -34,10 +35,11 @@ class UpdateCheck(QThread): class TrayIcon(QSystemTrayIcon): - def __init__(self, locale_keys: dict, controller: FlatpakController, check_interval: int = 60): + def __init__(self, locale_keys: dict, controller: FlatpakController, manager: FlatpakManager, check_interval: int = 60): super(TrayIcon, self).__init__() self.locale_keys = locale_keys self.controller = controller + self.manager = manager self.icon_default = QIcon(resource.get_path('img/flathub_45.svg')) self.icon_update = QIcon(resource.get_path('img/update_logo.svg')) @@ -50,7 +52,7 @@ class TrayIcon(QSystemTrayIcon): self.action_exit.triggered.connect(lambda: QCoreApplication.exit()) self.setContextMenu(self.menu) - self.manage_window = ManageWindow(locale_keys=self.locale_keys, controller=controller, tray_icon=self) + self.manage_window = ManageWindow(locale_keys=self.locale_keys, controller=controller, manager=self.manager, tray_icon=self) self.check_thread = UpdateCheck(check_interval=check_interval, controller=self.controller) self.check_thread.signal.connect(self.notify_updates) self.check_thread.start() diff --git a/fpakman/view/qt/window.py b/fpakman/view/qt/window.py index 258e6af2..b6b09a27 100755 --- a/fpakman/view/qt/window.py +++ b/fpakman/view/qt/window.py @@ -7,18 +7,21 @@ from PyQt5.QtCore import QThread, pyqtSignal, QEvent from PyQt5.QtGui import QIcon, QWindowStateChangeEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ QSizePolicy, QLabel, QMessageBox, QPlainTextEdit +from fpakman.core.model import FlatpakManager, ImpossibleDowngradeException from fpakman import __version__ from fpakman.core import resource from fpakman.core.controller import FlatpakController +from fpakman.view.qt import dialog from fpakman.view.qt.apps_table import AppsTable +from fpakman.view.qt.root import is_root, ask_root_password class ManageWindow(QWidget): __BASE_HEIGHT__ = 400 - def __init__(self, locale_keys: dict, controller: FlatpakController, tray_icon = None): + def __init__(self, locale_keys: dict, controller: FlatpakController, manager: FlatpakManager, tray_icon = None): super(ManageWindow, self).__init__() self.locale_keys = locale_keys self.column_names = [locale_keys['manage_window.columns.name'], @@ -30,6 +33,7 @@ class ManageWindow(QWidget): locale_keys['manage_window.columns.origin'], locale_keys['manage_window.columns.update']] self.controller = controller + self.manager = manager self.tray_icon = tray_icon self.thread_lock = Lock() self.working = False # restrict the number of threaded actions @@ -104,6 +108,10 @@ class ManageWindow(QWidget): self.thread_uninstall.signal_output.connect(self._update_action_output) self.thread_uninstall.signal_finished.connect(self._finish_uninstall) + self.thread_downgrade = DowngradeApp(self.manager) + self.thread_downgrade.signal_output.connect(self._update_action_output) + self.thread_downgrade.signal_finished.connect(self._finish_downgrade) + self.toolbar_bottom = QToolBar() self.label_updates = QLabel('') self.toolbar_bottom.addWidget(self.label_updates) @@ -132,12 +140,9 @@ class ManageWindow(QWidget): def _check_flatpak_installed(self): if not self.controller.check_installed(): - error_msg = QMessageBox() - error_msg.setIcon(QMessageBox.Critical) - error_msg.setWindowTitle(self.locale_keys['popup.flatpak_not_installed.title']) - error_msg.setText(self.locale_keys['popup.flatpak_not_installed.msg'] + '...') - error_msg.setWindowIcon(self.icon_flathub) - error_msg.exec_() + dialog.show_error(title=self.locale_keys['popup.flatpak_not_installed.title'], + body=self.locale_keys['popup.flatpak_not_installed.msg'] + '...', + icon=self.icon_flathub) exit(1) if self.label_flatpak: @@ -199,6 +204,11 @@ class ManageWindow(QWidget): self._release_lock() self.refresh(clear_output=False) + def _finish_downgrade(self): + self.finish_action() + self._release_lock() + self.refresh(clear_output=False) + def filter_only_apps(self, only_apps: int): if self.apps: @@ -318,6 +328,29 @@ class ManageWindow(QWidget): self.table_apps.setEnabled(True) self.label_status.setText('') + def downgrade_app(self, app_ref: str): + + self._check_flatpak_installed() + + if self._acquire_lock(): + + pwd = None + + if not is_root(): + pwd, ok = ask_root_password(self.locale_keys) + + if not ok: + self._release_lock() + return + + self.textarea_output.clear() + self.textarea_output.setVisible(True) + self._begin_action(self.locale_keys['manage_window.status.downgrading'] + '...') + + self.thread_downgrade.app_ref = app_ref + self.thread_downgrade.root_password = pwd + self.thread_downgrade.start() + # Threaded actions class UpdateSelectedApps(QThread): @@ -372,3 +405,31 @@ class UninstallApp(QThread): self.signal_output.emit(line) self.signal_finished.emit() + + +class DowngradeApp(QThread): + signal_finished = pyqtSignal() + signal_output = pyqtSignal(str) + + def __init__(self, manager: FlatpakManager): + super(DowngradeApp, self).__init__() + self.manager = manager + self.app_ref = None + self.root_password = None + + def run(self): + if self.app_ref: + try: + for output in self.manager.downgrade_app(self.app_ref, self.root_password): + line = output.decode().strip() + if line: + self.signal_output.emit(line) + + except ImpossibleDowngradeException: + dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], + body=self.locale_keys['popup.downgrade.impossible.body'], + icon=self.icon_flathub) + + self.app_ref = None + self.root_password = None + self.signal_finished.emit()