Downgrade

This commit is contained in:
Vinícius Moreira
2019-06-25 18:25:04 -03:00
committed by GitHub
parent 7d106c149b
commit f9f47d1429
15 changed files with 349 additions and 32 deletions

View File

@@ -6,13 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.3.0] ## [0.3.0]
### Features ### Features
- Allows app uninstall by right clicking it. - Allows app uninstall and downgrade by right clicking it.
### Improvements ### Improvements
- Adding tooltips to toolbar buttons - Adding tooltips to toolbar buttons
- "Update ?" column renamed to "Upgrade ?" - "Update ?" column renamed to "Upgrade ?"
- Management panel title renamed - Management panel title renamed
- Showing runtime apps when no app is available - 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 ## [0.2.1] - 2019-06-24
### Features ### Features

View File

@@ -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: 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_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_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 ### Roadmap
- Search and install applications - Search and install applications

View File

@@ -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__)) parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__))
args = parser.parse_args() 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 = QApplication(sys.argv)
app.setWindowIcon(QIcon(resource.get_path('img/flathub_45.svg'))) app.setWindowIcon(QIcon(resource.get_path('img/flathub_45.svg')))
@@ -26,6 +26,7 @@ controller = FlatpakController(manager)
trayIcon = TrayIcon(locale_keys=locale_keys, trayIcon = TrayIcon(locale_keys=locale_keys,
controller=controller, controller=controller,
manager=manager,
check_interval=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60))) check_interval=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)))
trayIcon.show() trayIcon.show()

View File

@@ -1,4 +1,5 @@
import re import re
import subprocess
from typing import List from typing import List
from fpakman.core import system from fpakman.core import system
@@ -32,20 +33,33 @@ def app_str_to_json(line: str, version: str) -> dict:
else: else:
raise Exception('Unsupported version') raise Exception('Unsupported version')
info = re.findall(r'\w+:\s.+', get_app_info(app['id'], app['branch'])) extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
fields_to_get = ['origin', 'arch', 'ref'] 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: for field in info:
if fields_to_retrieve == 0:
break
field_val = field.split(':') field_val = field.split(':')
field_name = field_val[0].lower() field_name = field_val[0].lower()
if field_name in fields_to_get: if field_name in fields or (check_runtime and field_name == 'ref'):
app[field_name] = field_val[1].strip() data[field_name] = field_val[1].strip()
fields_to_retrieve -= 1
if field_name == 'ref': if check_runtime and field_name == 'ref':
app['runtime'] = app['ref'].startswith('runtime/') data['runtime'] = data['ref'].startswith('runtime/')
return app return data
def get_version(): def get_version():
@@ -92,3 +106,20 @@ def uninstall_and_stream(app_ref: str):
def list_updates_as_str(): def list_updates_as_str():
return system.run_cmd('flatpak update', ignore_return_code=True) 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)

View File

@@ -2,9 +2,6 @@ from threading import Lock, Thread
from typing import List from typing import List
import requests import requests
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from fpakman.core import flatpak from fpakman.core import flatpak
@@ -12,6 +9,10 @@ __FLATHUB_URL__ = 'https://flathub.org'
__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' __FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1'
class ImpossibleDowngradeException(Exception):
pass
class FlatpakManager: class FlatpakManager:
def __init__(self): def __init__(self):
@@ -122,3 +123,20 @@ class FlatpakManager:
if app_found: if app_found:
return flatpak.uninstall_and_stream(ref) 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)

View File

@@ -4,11 +4,15 @@ from fpakman.core import resource
import glob import glob
def get_locale_keys(): def get_locale_keys(key: str = None):
current_locale = locale.getdefaultlocale()
locale_path = None locale_path = None
if key is None:
current_locale = locale.getdefaultlocale()
else:
current_locale = [key]
if current_locale: if current_locale:
current_locale = current_locale[0] current_locale = current_locale[0]

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
viewBox="0 0 48 48"
enable-background="new 0 0 26 26"
id="svg8"
sodipodi:docname="downgrade.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
width="48"
height="48">
<metadata
id="metadata14">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs12" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="739"
id="namedview10"
showgrid="false"
showborder="false"
inkscape:zoom="9.0769231"
inkscape:cx="-0.33050843"
inkscape:cy="20.813559"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="svg8"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="g6"
transform="matrix(1.8461538,0,0,2,0,-2)"
style="fill:#ff0000">
<path
d="m 25,17 h -2 c -0.6,0 -1,0.4 -1,1 v 2.5 C 22,20.8 21.8,21 21.5,21 H 4.5 C 4.2,21 4,20.8 4,20.5 V 18 C 4,17.4 3.6,17 3,17 H 1 c -0.6,0 -1,0.4 -1,1 v 6 c 0,0.6 0.4,1 1,1 h 24 c 0.6,0 1,-0.4 1,-1 v -6 c 0,-0.6 -0.4,-1 -1,-1 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:#ff0000" />
<path
d="m 12.3,16.7 c 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 l 6,-6 C 19.9,10.5 20,10.3 20,10 20,9.7 19.9,9.5 19.7,9.3 L 18.3,7.9 C 18.1,7.7 17.9,7.6 17.6,7.6 c -0.3,0 -0.5,0.1 -0.7,0.3 l -1,1 C 15.6,9.2 15,9 15,8.5 V 2 C 15,1.4 14.6,1 14,1 H 12 C 11.4,1 11,1.4 11,2 V 8.6 C 11,9 10.5,9.3 10.1,9 L 9.1,8 C 8.9,7.8 8.7,7.7 8.4,7.7 8.1,7.7 7.9,7.8 7.7,8 L 6.3,9.4 C 6.1,9.6 6,9.8 6,10.1 c 0,0.3 0.1,0.5 0.3,0.7 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ff0000" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -10,15 +10,27 @@ manage_window.columns.update=Upgrade ?
manage_window.apps_table.row.actions.uninstall=Uninstall 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.title=Uninstall
manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} ? 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.checkbox.only_apps=Apps
manage_window.label.updates=Updates manage_window.label.updates=Updates
manage_window.status.refreshing=Refreshing manage_window.status.refreshing=Refreshing
manage_window.status.upgrading=Upgrading manage_window.status.upgrading=Upgrading
manage_window.status.uninstalling=Uninstalling manage_window.status.uninstalling=Uninstalling
manage_window.status.downgrading=Downgrading
manage_window.bt.refresh.tooltip=Reload the data about installed applications manage_window.bt.refresh.tooltip=Reload the data about installed applications
manage_window.bt.upgrade.tooltip=Upgrade all selected applications manage_window.bt.upgrade.tooltip=Upgrade all selected applications
popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.title=Error
popup.flatpak_not_installed.msg=flatpak seems not to be installed. Exiting 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.manage=Manage applications
tray.action.exit=Exit tray.action.exit=Exit
notification.new_updates=Updates found notification.new_updates=Updates found

View File

@@ -2,7 +2,7 @@ manage_window.title=Administrador de Aplicativos Flatpak
manage_window.columns.name=Nombre manage_window.columns.name=Nombre
manage_window.columns.version=Versión manage_window.columns.version=Versión
manage_window.columns.latest_version=Ultima 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.arch=Arch
manage_window.columns.ref=Ref manage_window.columns.ref=Ref
manage_window.columns.origin=Origen 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=Desinstalar
manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación 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.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.checkbox.only_apps=Apps
manage_window.label.updates=Actualizaciones manage_window.label.updates=Actualizaciones
manage_window.status.refreshing=Recargando manage_window.status.refreshing=Recargando
manage_window.status.upgrading=Actualizando manage_window.status.upgrading=Actualizando
manage_window.status.uninstalling=Desinstalando 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.refresh.tooltip=Recarga los datos sobre los aplicativos instalados
manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados
popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.title=Error
popup.flatpak_not_installed.msg=flatpak no parece estar instalado. Saliendo 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.manage=Administrar aplicativos
tray.action.exit=Salir tray.action.exit=Salir
notification.new_updates=Actualizaciones encontradas notification.new_updates=Actualizaciones encontradas

View File

@@ -10,15 +10,27 @@ manage_window.columns.update=Atualizar ?
manage_window.apps_table.row.actions.uninstall=Desinstalar 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.title=Desinstalação
manage_window.apps_table.row.actions.uninstall.popup.body=Remover {}? 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.checkbox.only_apps=Apps
manage_window.label.updates=Atualizações manage_window.label.updates=Atualizações
manage_window.status.refreshing=Recarregando manage_window.status.refreshing=Recarregando
manage_window.status.upgrading=Atualizando manage_window.status.upgrading=Atualizando
manage_window.status.uninstalling=Desinstalando 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.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados
manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados
popup.flatpak_not_installed.title=Erro popup.flatpak_not_installed.title=Erro
popup.flatpak_not_installed.msg=flatpak não parece estar instalado. Saindo 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.manage=Gerenciar aplicativos
tray.action.exit=Sair tray.action.exit=Sair
notification.new_updates=Atualizações encontradas notification.new_updates=Atualizações encontradas

View File

@@ -8,6 +8,7 @@ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidg
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.controller import FlatpakController from fpakman.core.controller import FlatpakController
from fpakman.view.qt import dialog
class UpdateToggleButton(QToolButton): class UpdateToggleButton(QToolButton):
@@ -48,6 +49,7 @@ class AppsTable(QTableWidget):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.icon_flathub = QIcon(resource.get_path('img/flathub_45.svg')) self.icon_flathub = QIcon(resource.get_path('img/flathub_45.svg'))
self.icon_uninstall = QIcon(resource.get_path('img/uninstall.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 = QNetworkAccessManager()
self.network_man.finished.connect(self._load_icon) 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 = QAction(self.parent.locale_keys["manage_window.apps_table.row.actions.uninstall"])
action_uninstall.setIcon(self.icon_uninstall) action_uninstall.setIcon(self.icon_uninstall)
action_uninstall.triggered.connect(self._uninstall_app) action_uninstall.triggered.connect(self._uninstall_app)
menu_row.addAction(action_uninstall) 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.adjustSize()
menu_row.popup(QCursor.pos()) menu_row.popup(QCursor.pos())
menu_row.exec_() menu_row.exec_()
def _uninstall_app(self): def _get_selected_app(self):
selected_app = self.parent.apps[self.currentRow()] return self.parent.apps[self.currentRow()]
confirmation = QMessageBox.question(self, self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], def _uninstall_app(self):
self.parent.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), selected_app = self._get_selected_app()
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirmation == QMessageBox.Yes: 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']) 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): def _load_icon(self, http_response):
icon_url = http_response.url().toString() icon_url = http_response.url().toString()
pixmap = QPixmap() pixmap = QPixmap()
@@ -122,7 +142,7 @@ class AppsTable(QTableWidget):
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.setItem(idx, 2, col_release) 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_release.setForeground(QColor('orange'))
col_branch = QTableWidgetItem() col_branch = QTableWidgetItem()

31
fpakman/view/qt/dialog.py Normal file
View File

@@ -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

39
fpakman/view/qt/root.py Normal file
View File

@@ -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())

View File

@@ -4,6 +4,7 @@ import time
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from fpakman.core.model import FlatpakManager
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.controller import FlatpakController from fpakman.core.controller import FlatpakController
@@ -34,10 +35,11 @@ class UpdateCheck(QThread):
class TrayIcon(QSystemTrayIcon): 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__() super(TrayIcon, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.controller = controller self.controller = controller
self.manager = manager
self.icon_default = QIcon(resource.get_path('img/flathub_45.svg')) self.icon_default = QIcon(resource.get_path('img/flathub_45.svg'))
self.icon_update = QIcon(resource.get_path('img/update_logo.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.action_exit.triggered.connect(lambda: QCoreApplication.exit())
self.setContextMenu(self.menu) 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 = UpdateCheck(check_interval=check_interval, controller=self.controller)
self.check_thread.signal.connect(self.notify_updates) self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start() self.check_thread.start()

View File

@@ -7,18 +7,21 @@ from PyQt5.QtCore import QThread, pyqtSignal, QEvent
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent from PyQt5.QtGui import QIcon, QWindowStateChangeEvent
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QSizePolicy, QLabel, QMessageBox, QPlainTextEdit QSizePolicy, QLabel, QMessageBox, QPlainTextEdit
from fpakman.core.model import FlatpakManager, ImpossibleDowngradeException
from fpakman import __version__ from fpakman import __version__
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.controller import FlatpakController from fpakman.core.controller import FlatpakController
from fpakman.view.qt import dialog
from fpakman.view.qt.apps_table import AppsTable from fpakman.view.qt.apps_table import AppsTable
from fpakman.view.qt.root import is_root, ask_root_password
class ManageWindow(QWidget): class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400 __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__() super(ManageWindow, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.column_names = [locale_keys['manage_window.columns.name'], 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.origin'],
locale_keys['manage_window.columns.update']] locale_keys['manage_window.columns.update']]
self.controller = controller self.controller = controller
self.manager = manager
self.tray_icon = tray_icon self.tray_icon = tray_icon
self.thread_lock = Lock() self.thread_lock = Lock()
self.working = False # restrict the number of threaded actions 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_output.connect(self._update_action_output)
self.thread_uninstall.signal_finished.connect(self._finish_uninstall) 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.toolbar_bottom = QToolBar()
self.label_updates = QLabel('') self.label_updates = QLabel('')
self.toolbar_bottom.addWidget(self.label_updates) self.toolbar_bottom.addWidget(self.label_updates)
@@ -132,12 +140,9 @@ class ManageWindow(QWidget):
def _check_flatpak_installed(self): def _check_flatpak_installed(self):
if not self.controller.check_installed(): if not self.controller.check_installed():
error_msg = QMessageBox() dialog.show_error(title=self.locale_keys['popup.flatpak_not_installed.title'],
error_msg.setIcon(QMessageBox.Critical) body=self.locale_keys['popup.flatpak_not_installed.msg'] + '...',
error_msg.setWindowTitle(self.locale_keys['popup.flatpak_not_installed.title']) icon=self.icon_flathub)
error_msg.setText(self.locale_keys['popup.flatpak_not_installed.msg'] + '...')
error_msg.setWindowIcon(self.icon_flathub)
error_msg.exec_()
exit(1) exit(1)
if self.label_flatpak: if self.label_flatpak:
@@ -199,6 +204,11 @@ class ManageWindow(QWidget):
self._release_lock() self._release_lock()
self.refresh(clear_output=False) 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): def filter_only_apps(self, only_apps: int):
if self.apps: if self.apps:
@@ -318,6 +328,29 @@ class ManageWindow(QWidget):
self.table_apps.setEnabled(True) self.table_apps.setEnabled(True)
self.label_status.setText('') 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 # Threaded actions
class UpdateSelectedApps(QThread): class UpdateSelectedApps(QThread):
@@ -372,3 +405,31 @@ class UninstallApp(QThread):
self.signal_output.emit(line) self.signal_output.emit(line)
self.signal_finished.emit() 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()