mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-08 07:24:15 +02:00
0.3.0
### Features
- Applications search
- Now when you right-click a selected application you can:
- retrieve its information
- retrieve its commit history
- downgrade
- install and uninstall it
- "About" window available when right-clicking the tray icon.
### Improvements
- Performance and memory usage
- 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**
- Adding expiration time for cached app data. Default to 1 hour. The environment variable **FPAKMAN_CACHE_EXPIRATION** can change this value.
- Now the application accepts arguments related to environment variables as well. Check 'README.md'.
- Minor GUI improvements
- Notifying only new updates
- New icon
- Progress bar
This commit is contained in:
71
fpakman/view/qt/about.py
Normal file
71
fpakman/view/qt/about.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel
|
||||
|
||||
from fpakman import __version__
|
||||
from fpakman.core import resource
|
||||
|
||||
PROJECT_URL = 'https://github.com/vinifmor/fpakman'
|
||||
LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/fpakman/master/LICENSE'
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
|
||||
def __init__(self, locale_keys: dict):
|
||||
super(AboutDialog, self).__init__()
|
||||
self.setWindowTitle(locale_keys['tray.action.about'])
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
label_logo = QLabel(self)
|
||||
label_logo.setPixmap(QPixmap(resource.get_path('img/logo.svg')))
|
||||
label_logo.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_logo)
|
||||
|
||||
label_name = QLabel('fpakman ( {} {} )'.format(locale_keys['flatpak.info.version'].lower(), __version__))
|
||||
label_name.setStyleSheet('font-weight: bold;')
|
||||
label_name.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_name)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
line_desc = QLabel(self)
|
||||
line_desc.setStyleSheet('font-size: 10px; font-weight: bold;')
|
||||
line_desc.setText(locale_keys['about.info.desc'])
|
||||
line_desc.setAlignment(Qt.AlignCenter)
|
||||
line_desc.setMinimumWidth(400)
|
||||
layout.addWidget(line_desc)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_more_info = QLabel()
|
||||
label_more_info.setStyleSheet('font-size: 9px;')
|
||||
label_more_info.setText(locale_keys['about.info.link'] + ": <a href='{url}'>{url}</a>".format(url=PROJECT_URL))
|
||||
label_more_info.setOpenExternalLinks(True)
|
||||
label_more_info.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_more_info)
|
||||
|
||||
label_license = QLabel()
|
||||
label_license.setStyleSheet('font-size: 9px;')
|
||||
label_license.setText("<a href='{}'>{}</a>".format(LICENSE_URL, locale_keys['about.info.license']))
|
||||
label_license.setOpenExternalLinks(True)
|
||||
label_license.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_license)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
label_rate = QLabel()
|
||||
label_rate.setStyleSheet('font-size: 9px; font-weight: bold;')
|
||||
label_rate.setText(locale_keys['about.info.rate'] + ' :)')
|
||||
label_rate.setOpenExternalLinks(True)
|
||||
label_rate.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(label_rate)
|
||||
|
||||
layout.addWidget(QLabel(''))
|
||||
|
||||
self.adjustSize()
|
||||
self.setFixedSize(self.size())
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
225
fpakman/view/qt/apps_table.py
Normal file
225
fpakman/view/qt/apps_table.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
|
||||
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
|
||||
QHeaderView, QLabel
|
||||
|
||||
from fpakman.core import resource, util
|
||||
from fpakman.view.qt import dialog
|
||||
|
||||
|
||||
class UpdateToggleButton(QToolButton):
|
||||
|
||||
def __init__(self, model: dict, root: QWidget, locale_keys: dict, checked: bool = True):
|
||||
super(UpdateToggleButton, self).__init__()
|
||||
self.app = model
|
||||
self.root = root
|
||||
self.setCheckable(True)
|
||||
self.clicked.connect(self.change_state)
|
||||
self.icon_on = QIcon(resource.get_path('img/toggle_on.svg'))
|
||||
self.icon_off = QIcon(resource.get_path('img/toggle_off.svg'))
|
||||
self.setIcon(self.icon_on)
|
||||
self.setStyleSheet('border: 0px;')
|
||||
self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip'])
|
||||
|
||||
if not checked:
|
||||
self.click()
|
||||
|
||||
def change_state(self, not_checked: bool):
|
||||
self.app['update_checked'] = not not_checked
|
||||
self.setIcon(self.icon_on if not not_checked else self.icon_off)
|
||||
self.root.change_update_state()
|
||||
|
||||
|
||||
class AppsTable(QTableWidget):
|
||||
|
||||
def __init__(self, parent: QWidget):
|
||||
super(AppsTable, self).__init__()
|
||||
self.setParent(parent)
|
||||
self.window = parent
|
||||
self.column_names = [parent.locale_keys[key].capitalize() for key in ['flatpak.info.name',
|
||||
'flatpak.info.version',
|
||||
'manage_window.columns.latest_version',
|
||||
'flatpak.info.branch',
|
||||
'flatpak.info.arch',
|
||||
'flatpak.info.id',
|
||||
'flatpak.info.origin',
|
||||
'manage_window.columns.installed',
|
||||
'manage_window.columns.update']]
|
||||
self.setColumnCount(len(self.column_names))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
self.setShowGrid(False)
|
||||
self.verticalHeader().setVisible(False)
|
||||
self.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.setHorizontalHeaderLabels(self.column_names)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.icon_flathub = QIcon(resource.get_path('img/flathub.svg'))
|
||||
|
||||
self.network_man = QNetworkAccessManager()
|
||||
self.network_man.finished.connect(self._load_icon)
|
||||
|
||||
self.icon_cache = {}
|
||||
|
||||
def contextMenuEvent(self, QContextMenuEvent): # selected row right click event
|
||||
|
||||
app = self.get_selected_app()
|
||||
|
||||
menu_row = QMenu()
|
||||
|
||||
if app['model']['installed']:
|
||||
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
|
||||
action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
|
||||
action_info.triggered.connect(self._get_app_info)
|
||||
menu_row.addAction(action_info)
|
||||
|
||||
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
|
||||
action_history.setIcon(QIcon(resource.get_path('img/history.svg')))
|
||||
action_history.triggered.connect(self._get_app_history)
|
||||
menu_row.addAction(action_history)
|
||||
|
||||
action_uninstall = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.uninstall"])
|
||||
action_uninstall.setIcon(QIcon(resource.get_path('img/uninstall.svg')))
|
||||
action_uninstall.triggered.connect(self._uninstall_app)
|
||||
menu_row.addAction(action_uninstall)
|
||||
|
||||
if not app['model']['runtime']: # not available for runtimes
|
||||
action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"])
|
||||
action_downgrade.triggered.connect(self._downgrade_app)
|
||||
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
|
||||
menu_row.addAction(action_downgrade)
|
||||
else:
|
||||
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
|
||||
action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
|
||||
action_install.triggered.connect(self._install_app)
|
||||
menu_row.addAction(action_install)
|
||||
|
||||
menu_row.adjustSize()
|
||||
menu_row.popup(QCursor.pos())
|
||||
menu_row.exec_()
|
||||
|
||||
def get_selected_app(self):
|
||||
return self.window.apps[self.currentRow()]
|
||||
|
||||
def get_selected_app_icon(self):
|
||||
return self.item(self.currentRow(), 0).icon()
|
||||
|
||||
def _uninstall_app(self):
|
||||
selected_app = self.get_selected_app()
|
||||
|
||||
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'],
|
||||
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']),
|
||||
locale_keys=self.window.locale_keys):
|
||||
self.window.uninstall_app(selected_app['model']['ref'])
|
||||
|
||||
def _downgrade_app(self):
|
||||
selected_app = self.get_selected_app()
|
||||
|
||||
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
|
||||
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']),
|
||||
locale_keys=self.window.locale_keys):
|
||||
self.window.downgrade_app(selected_app)
|
||||
|
||||
def _get_app_info(self):
|
||||
self.window.get_app_info(self.get_selected_app())
|
||||
|
||||
def _get_app_history(self):
|
||||
self.window.get_app_history(self.get_selected_app())
|
||||
|
||||
def _install_app(self):
|
||||
self.window.install_app(self.get_selected_app())
|
||||
|
||||
def _load_icon(self, http_response):
|
||||
icon_url = http_response.url().toString()
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(http_response.readAll())
|
||||
icon = QIcon(pixmap)
|
||||
self.icon_cache[icon_url] = icon
|
||||
|
||||
for idx, app in enumerate(self.window.apps):
|
||||
if app['model']['icon'] == icon_url:
|
||||
self.item(idx, 0).setIcon(icon)
|
||||
self.window.resize_and_center()
|
||||
break
|
||||
|
||||
def update_apps(self, apps: List[dict]):
|
||||
self.setEnabled(True)
|
||||
self.setRowCount(len(apps) if apps else 0)
|
||||
|
||||
if apps:
|
||||
for idx, app in enumerate(apps):
|
||||
|
||||
tooltip = util.strip_html(app['model']['description']) if app['model']['description'] else None
|
||||
|
||||
col_name = QTableWidgetItem()
|
||||
col_name.setText(app['model']['name'])
|
||||
col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_name.setToolTip(tooltip)
|
||||
|
||||
if not app['model']['icon']:
|
||||
col_name.setIcon(self.icon_flathub)
|
||||
else:
|
||||
cached_icon = self.icon_cache.get(app['model']['icon'])
|
||||
|
||||
if cached_icon:
|
||||
col_name.setIcon(cached_icon)
|
||||
else:
|
||||
col_name.setIcon(self.icon_flathub)
|
||||
self.network_man.get(QNetworkRequest(QUrl(app['model']['icon'])))
|
||||
|
||||
self.setItem(idx, 0, col_name)
|
||||
|
||||
col_version = QTableWidgetItem()
|
||||
col_version.setText(app['model']['version'])
|
||||
col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_version.setToolTip(tooltip)
|
||||
self.setItem(idx, 1, col_version)
|
||||
|
||||
col_release = QTableWidgetItem()
|
||||
col_release.setText(app['model']['latest_version'])
|
||||
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_release.setToolTip(tooltip)
|
||||
self.setItem(idx, 2, col_release)
|
||||
|
||||
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()
|
||||
col_branch.setText(app['model']['branch'])
|
||||
col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_branch.setToolTip(tooltip)
|
||||
self.setItem(idx, 3, col_branch)
|
||||
|
||||
col_arch = QTableWidgetItem()
|
||||
col_arch.setText(app['model']['arch'])
|
||||
col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_arch.setToolTip(tooltip)
|
||||
self.setItem(idx, 4, col_arch)
|
||||
|
||||
col_id = QTableWidgetItem()
|
||||
col_id.setText(app['model']['id'])
|
||||
col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_id.setToolTip(tooltip)
|
||||
self.setItem(idx, 5, col_id)
|
||||
|
||||
col_origin = QTableWidgetItem()
|
||||
col_origin.setText(app['model']['origin'])
|
||||
col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
col_origin.setToolTip(tooltip)
|
||||
self.setItem(idx, 6, col_origin)
|
||||
|
||||
col_installed = QLabel()
|
||||
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross')))))
|
||||
col_installed.setToolTip(tooltip)
|
||||
col_installed.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.setCellWidget(idx, 7, col_installed)
|
||||
|
||||
col_update = UpdateToggleButton(app, self.window, self.window.locale_keys, app['model']['update']) if app['model']['update'] else None
|
||||
self.setCellWidget(idx, 8, col_update)
|
||||
|
||||
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
|
||||
header_horizontal = self.horizontalHeader()
|
||||
for i in range(self.columnCount()):
|
||||
header_horizontal.setSectionResizeMode(i, policy)
|
||||
32
fpakman/view/qt/dialog.py
Normal file
32
fpakman/view/qt/dialog.py
Normal file
@@ -0,0 +1,32 @@
|
||||
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/logo.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/logo.svg'))):
|
||||
dialog_confirmation = QMessageBox()
|
||||
dialog_confirmation.setIcon(QMessageBox.Question)
|
||||
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
|
||||
54
fpakman/view/qt/history.py
Normal file
54
fpakman/view/qt/history.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
|
||||
|
||||
|
||||
class HistoryDialog(QDialog):
|
||||
|
||||
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
|
||||
super(HistoryDialog, self).__init__()
|
||||
|
||||
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name']))
|
||||
self.setWindowIcon(app_icon)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
table_history = QTableWidget()
|
||||
table_history.setFocusPolicy(Qt.NoFocus)
|
||||
table_history.setShowGrid(False)
|
||||
table_history.verticalHeader().setVisible(False)
|
||||
table_history.setAlternatingRowColors(True)
|
||||
|
||||
table_history.setColumnCount(len(app['commits'][0]))
|
||||
table_history.setRowCount(len(app['commits']))
|
||||
table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['commits'][0].keys())])
|
||||
|
||||
for row, commit in enumerate(app['commits']):
|
||||
|
||||
current_app_commit = app['model']['commit'] == commit['commit']
|
||||
|
||||
for col, key in enumerate(sorted(commit.keys())):
|
||||
item = QTableWidgetItem()
|
||||
item.setText(commit[key])
|
||||
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
|
||||
if current_app_commit:
|
||||
item.setBackground(Qt.darkYellow if row != 0 else Qt.darkGreen)
|
||||
tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize())
|
||||
|
||||
item.setToolTip(tip)
|
||||
|
||||
table_history.setItem(row, col, item)
|
||||
|
||||
layout.addWidget(table_history)
|
||||
|
||||
header_horizontal = table_history.horizontalHeader()
|
||||
for i in range(0, table_history.columnCount()):
|
||||
header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
|
||||
|
||||
new_width = reduce(operator.add, [table_history.columnWidth(i) for i in range(table_history.columnCount())])
|
||||
self.resize(new_width, table_history.height())
|
||||
39
fpakman/view/qt/info.py
Normal file
39
fpakman/view/qt/info.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \
|
||||
QLineEdit, QLabel, QPlainTextEdit
|
||||
|
||||
|
||||
class InfoDialog(QDialog):
|
||||
|
||||
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
|
||||
super(InfoDialog, self).__init__()
|
||||
self.setWindowTitle(app['name'])
|
||||
self.setWindowIcon(app_icon)
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
gbox_info_layout = QFormLayout()
|
||||
gbox_info = QGroupBox()
|
||||
gbox_info.setLayout(gbox_info_layout)
|
||||
layout.addWidget(gbox_info)
|
||||
|
||||
for attr in sorted(app.keys()):
|
||||
|
||||
if attr != 'name' and app[attr]:
|
||||
if attr == 'description':
|
||||
text = QPlainTextEdit()
|
||||
text.appendHtml(app[attr])
|
||||
else:
|
||||
text = QLineEdit()
|
||||
text.setText(app[attr])
|
||||
text.setCursorPosition(0)
|
||||
text.setStyleSheet("width: 400px")
|
||||
|
||||
text.setReadOnly(True)
|
||||
|
||||
label = QLabel("{}: ".format(locale_keys.get('flatpak.info.' + attr, attr)).capitalize())
|
||||
label.setStyleSheet("font-weight: bold")
|
||||
|
||||
gbox_info_layout.addRow(label, text)
|
||||
|
||||
self.adjustSize()
|
||||
45
fpakman/view/qt/root.py
Normal file
45
fpakman/view/qt/root.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import io
|
||||
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:
|
||||
proc = subprocess.Popen('sudo -k && echo {} | sudo -S whoami'.format(password),
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
bufsize=-1)
|
||||
stream = os._wrap_close(io.TextIOWrapper(proc.stdout), proc)
|
||||
res = stream.read()
|
||||
stream.close()
|
||||
|
||||
return bool(res.strip())
|
||||
@@ -1,82 +1,139 @@
|
||||
import os
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
||||
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.controller import FlatpakController
|
||||
from fpakman.core import resource, system
|
||||
from fpakman.core.controller import FlatpakManager
|
||||
from fpakman.view.qt.about import AboutDialog
|
||||
from fpakman.view.qt.window import ManageWindow
|
||||
|
||||
|
||||
class UpdateCheck(QThread):
|
||||
|
||||
signal = pyqtSignal(int)
|
||||
signal = pyqtSignal(list)
|
||||
|
||||
def __init__(self, check_interval: int, controller: FlatpakController, parent=None):
|
||||
def __init__(self, manager: FlatpakManager, check_interval: int, parent=None):
|
||||
super(UpdateCheck, self).__init__(parent)
|
||||
self.controller = controller
|
||||
self.check_interval = check_interval
|
||||
self.manager = manager
|
||||
|
||||
def run(self):
|
||||
|
||||
while True:
|
||||
|
||||
apps = self.controller.refresh()
|
||||
apps = self.manager.read_installed()
|
||||
|
||||
updates = len([app for app in apps if app['update']])
|
||||
updates = [app for app in apps if app['update']]
|
||||
|
||||
self.signal.emit(updates)
|
||||
if updates:
|
||||
self.signal.emit(updates)
|
||||
|
||||
time.sleep(self.check_interval)
|
||||
|
||||
|
||||
class LoadDatabase(QThread):
|
||||
|
||||
signal_finished = pyqtSignal()
|
||||
|
||||
def __init__(self, manager: FlatpakManager, parent=None):
|
||||
super(LoadDatabase, self).__init__(parent)
|
||||
self.manager = manager
|
||||
|
||||
def run(self):
|
||||
self.manager.load_full_database()
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
|
||||
def __init__(self, locale_keys: dict, controller: FlatpakController, check_interval: int = 60):
|
||||
def __init__(self, locale_keys: dict, manager: FlatpakManager, check_interval: int = 60, update_notification: bool = True):
|
||||
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'))
|
||||
self.icon_default = QIcon(resource.get_path('img/logo.png'))
|
||||
self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
|
||||
self.setIcon(self.icon_default)
|
||||
|
||||
self.menu = QMenu()
|
||||
|
||||
self.action_refreshing = self.menu.addAction(self.locale_keys['tray.action.refreshing'] + '...')
|
||||
self.action_refreshing.setEnabled(False)
|
||||
|
||||
self.action_manage = self.menu.addAction(self.locale_keys['tray.action.manage'])
|
||||
self.action_manage.triggered.connect(self.show_manage_window)
|
||||
self.action_manage.setVisible(False)
|
||||
|
||||
self.action_about = self.menu.addAction(self.locale_keys['tray.action.about'])
|
||||
self.action_about.triggered.connect(self.show_about)
|
||||
|
||||
self.action_exit = self.menu.addAction(self.locale_keys['tray.action.exit'])
|
||||
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.check_thread = UpdateCheck(check_interval=check_interval, controller=self.controller)
|
||||
self.manage_window = None
|
||||
self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager)
|
||||
self.check_thread.signal.connect(self.notify_updates)
|
||||
self.check_thread.start()
|
||||
|
||||
def notify_updates(self, updates: int):
|
||||
if updates > 0:
|
||||
if self.icon().cacheKey() != self.icon_update.cacheKey():
|
||||
self.setIcon(self.icon_update)
|
||||
self.dialog_about = None
|
||||
|
||||
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], updates)
|
||||
self.thread_database = LoadDatabase(manager)
|
||||
self.thread_database.signal_finished.connect(self._update_menu)
|
||||
self.last_updates = set()
|
||||
self.update_notification = update_notification
|
||||
|
||||
def load_database(self):
|
||||
self.thread_database.start()
|
||||
|
||||
def _update_menu(self):
|
||||
self.action_refreshing.setVisible(False)
|
||||
self.action_manage.setVisible(True)
|
||||
|
||||
def notify_updates(self, updates: List[dict]):
|
||||
|
||||
if len(updates) > 0:
|
||||
|
||||
update_keys = {'{}:{}'.format(app['id'], app['latest_version']) for app in updates}
|
||||
|
||||
new_icon = self.icon_update
|
||||
|
||||
if update_keys.difference(self.last_updates):
|
||||
self.last_updates = update_keys
|
||||
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'].format('Flatpak'), len(updates))
|
||||
self.setToolTip(msg)
|
||||
|
||||
if bool(os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1)):
|
||||
os.system("notify-send -i {} '{}'".format(resource.get_path('img/flathub_45.svg'), msg))
|
||||
|
||||
if self.manage_window:
|
||||
self.manage_window.refresh()
|
||||
if self.update_notification:
|
||||
system.notify_user(msg)
|
||||
|
||||
else:
|
||||
self.setIcon(self.icon_default)
|
||||
new_icon = self.icon_default
|
||||
self.setToolTip(None)
|
||||
|
||||
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
|
||||
self.setIcon(new_icon)
|
||||
|
||||
def show_manage_window(self):
|
||||
|
||||
if not self.manage_window:
|
||||
self.manage_window = ManageWindow(controller=self.controller)
|
||||
if self.manage_window is None:
|
||||
self.manage_window = ManageWindow(locale_keys=self.locale_keys,
|
||||
manager=self.manager,
|
||||
tray_icon=self)
|
||||
|
||||
self.manage_window.refresh()
|
||||
self.manage_window.show()
|
||||
if self.manage_window.isMinimized():
|
||||
self.manage_window.setWindowState(Qt.WindowNoState)
|
||||
else:
|
||||
self.manage_window.refresh()
|
||||
self.manage_window.show()
|
||||
|
||||
def show_about(self):
|
||||
|
||||
if self.dialog_about is None:
|
||||
self.dialog_about = AboutDialog(self.locale_keys)
|
||||
|
||||
if self.dialog_about.isHidden():
|
||||
self.dialog_about.show()
|
||||
|
||||
186
fpakman/view/qt/thread.py
Normal file
186
fpakman/view/qt/thread.py
Normal file
@@ -0,0 +1,186 @@
|
||||
import time
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
|
||||
from fpakman.core import flatpak
|
||||
from fpakman.core.controller import FlatpakManager
|
||||
from fpakman.view.qt import dialog
|
||||
|
||||
|
||||
class UpdateSelectedApps(QThread):
|
||||
|
||||
signal_finished = pyqtSignal()
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super(UpdateSelectedApps, self).__init__()
|
||||
self.refs_to_update = []
|
||||
|
||||
def run(self):
|
||||
|
||||
for app_ref in self.refs_to_update:
|
||||
for output in flatpak.update_and_stream(app_ref):
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class RefreshApps(QThread):
|
||||
|
||||
signal = pyqtSignal(list)
|
||||
|
||||
def __init__(self, manager: FlatpakManager):
|
||||
super(RefreshApps, self).__init__()
|
||||
self.manager = manager
|
||||
|
||||
def run(self):
|
||||
self.signal.emit(self.manager.read_installed())
|
||||
|
||||
|
||||
class UninstallApp(QThread):
|
||||
signal_finished = pyqtSignal()
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super(UninstallApp, self).__init__()
|
||||
self.app_ref = None
|
||||
|
||||
def run(self):
|
||||
if self.app_ref:
|
||||
for output in flatpak.uninstall_and_stream(self.app_ref):
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class DowngradeApp(QThread):
|
||||
signal_finished = pyqtSignal()
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self, manager: FlatpakManager, locale_keys: dict):
|
||||
super(DowngradeApp, self).__init__()
|
||||
self.manager = manager
|
||||
self.app = None
|
||||
self.root_password = None
|
||||
self.locale_keys = locale_keys
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
|
||||
stream = self.manager.downgrade_app(self.app['model'], self.root_password)
|
||||
|
||||
if stream is None:
|
||||
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
|
||||
body=self.locale_keys['popup.downgrade.impossible.body'])
|
||||
else:
|
||||
for output in self.manager.downgrade_app(self.app['model'], self.root_password):
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
|
||||
self.app = None
|
||||
self.root_password = None
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class GetAppInfo(QThread):
|
||||
signal_finished = pyqtSignal(dict)
|
||||
|
||||
def __init__(self):
|
||||
super(GetAppInfo, self).__init__()
|
||||
self.app = None
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch'])
|
||||
app_info['name'] = self.app['model']['name']
|
||||
app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app'
|
||||
app_info['description'] = self.app['model']['description']
|
||||
self.signal_finished.emit(app_info)
|
||||
self.app = None
|
||||
|
||||
|
||||
class GetAppHistory(QThread):
|
||||
signal_finished = pyqtSignal(dict)
|
||||
|
||||
def __init__(self):
|
||||
super(GetAppHistory, self).__init__()
|
||||
self.app = None
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin'])
|
||||
self.signal_finished.emit({'model': self.app['model'], 'commits': commits})
|
||||
self.app = None
|
||||
|
||||
|
||||
class SearchApps(QThread):
|
||||
signal_finished = pyqtSignal(list)
|
||||
|
||||
def __init__(self, manager: FlatpakManager):
|
||||
super(SearchApps, self).__init__()
|
||||
self.word = None
|
||||
self.manager = manager
|
||||
|
||||
def run(self):
|
||||
apps_found = []
|
||||
|
||||
if self.word:
|
||||
apps_found = self.manager.search(self.word)
|
||||
|
||||
self.signal_finished.emit(apps_found)
|
||||
self.word = None
|
||||
|
||||
|
||||
class InstallApp(QThread):
|
||||
|
||||
signal_finished = pyqtSignal()
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self):
|
||||
super(InstallApp, self).__init__()
|
||||
self.app = None
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.app:
|
||||
for output in flatpak.install_and_stream(self.app['model']['id'], self.app['model']['origin']):
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
|
||||
self.app = None
|
||||
self.signal_finished.emit()
|
||||
|
||||
|
||||
class AnimateProgress(QThread):
|
||||
|
||||
signal_change = pyqtSignal(int)
|
||||
|
||||
def __init__(self):
|
||||
super(AnimateProgress, self).__init__()
|
||||
self.progress_value = 0
|
||||
self.increment = 5
|
||||
self.stop = False
|
||||
|
||||
def run(self):
|
||||
|
||||
current_increment = self.increment
|
||||
|
||||
while not self.stop:
|
||||
self.signal_change.emit(self.progress_value)
|
||||
|
||||
if self.progress_value == 100:
|
||||
current_increment = -current_increment
|
||||
if self.progress_value == 0:
|
||||
current_increment = self.increment
|
||||
|
||||
self.progress_value += current_increment
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
self.progress_value = 0
|
||||
@@ -3,81 +3,84 @@ from functools import reduce
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl, QEvent
|
||||
from PyQt5.QtGui import QIcon, QColor, QPixmap, QWindowStateChangeEvent
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QTableWidget, \
|
||||
QTableWidgetItem, QTableView, QCheckBox, QHeaderView, QToolButton, QToolBar, \
|
||||
QSizePolicy, QLabel, QMessageBox, QPlainTextEdit
|
||||
from PyQt5.QtCore import QEvent
|
||||
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
|
||||
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar
|
||||
|
||||
from fpakman import __version__
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.controller import FlatpakController
|
||||
from fpakman.core import resource, flatpak
|
||||
from fpakman.core.controller import FlatpakManager
|
||||
from fpakman.view.qt import dialog
|
||||
from fpakman.view.qt.apps_table import AppsTable
|
||||
from fpakman.view.qt.history import HistoryDialog
|
||||
from fpakman.view.qt.info import InfoDialog
|
||||
from fpakman.view.qt.root import is_root, ask_root_password
|
||||
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
|
||||
GetAppHistory, SearchApps, InstallApp, AnimateProgress
|
||||
|
||||
|
||||
class UpdateToggleButton(QToolButton):
|
||||
|
||||
def __init__(self, model: dict, root: QWidget, checked: bool = True):
|
||||
super(UpdateToggleButton, self).__init__()
|
||||
self.model = model
|
||||
self.root = root
|
||||
self.setCheckable(True)
|
||||
self.clicked.connect(self.change_state)
|
||||
self.icon_on = QIcon(resource.get_path('img/toggle_on.svg'))
|
||||
self.icon_off = QIcon(resource.get_path('img/toggle_off.svg'))
|
||||
self.setIcon(self.icon_on)
|
||||
self.setStyleSheet('border: 0px;')
|
||||
|
||||
if not checked:
|
||||
self.click()
|
||||
|
||||
def change_state(self, not_checked: bool):
|
||||
self.model['update_checked'] = not not_checked
|
||||
self.setIcon(self.icon_on if not not_checked else self.icon_off)
|
||||
self.root.change_update_state()
|
||||
DARK_ORANGE = '#FF4500'
|
||||
|
||||
|
||||
class ManageWindow(QWidget):
|
||||
|
||||
__BASE_HEIGHT__ = 400
|
||||
|
||||
def __init__(self, locale_keys: dict, controller: FlatpakController, tray_icon = None):
|
||||
def __init__(self, locale_keys: dict, manager: FlatpakManager, tray_icon=None):
|
||||
super(ManageWindow, self).__init__()
|
||||
self.locale_keys = locale_keys
|
||||
self.column_names = [locale_keys['manage_window.columns.name'],
|
||||
locale_keys['manage_window.columns.version'],
|
||||
locale_keys['manage_window.columns.latest_version'],
|
||||
locale_keys['manage_window.columns.branch'],
|
||||
locale_keys['manage_window.columns.arch'],
|
||||
locale_keys['manage_window.columns.ref'],
|
||||
locale_keys['manage_window.columns.origin'],
|
||||
locale_keys['manage_window.columns.update']]
|
||||
self.controller = controller
|
||||
self.icon_cache = {}
|
||||
self.manager = manager
|
||||
self.tray_icon = tray_icon
|
||||
self.thread_lock = Lock()
|
||||
self.working = False # restrict the number of threaded actions
|
||||
self.apps = []
|
||||
self.label_flatpak = None
|
||||
|
||||
self.network_man = QNetworkAccessManager()
|
||||
self.network_man.finished.connect(self._load_icon)
|
||||
|
||||
self.icon_flathub = QIcon(resource.get_path('img/flathub_45.svg'))
|
||||
self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
|
||||
self._check_flatpak_installed()
|
||||
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
|
||||
self.setWindowTitle('fpakman ({})'.format(__version__))
|
||||
self.setWindowTitle(locale_keys['manage_window.title'])
|
||||
self.setWindowIcon(self.icon_flathub)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.toolbar_search = QToolBar()
|
||||
self.toolbar_search.setStyleSheet("spacing: 0px;")
|
||||
self.toolbar_search.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
spacer = QWidget()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
self.toolbar_search.addWidget(spacer)
|
||||
|
||||
label_pre_search = QLabel()
|
||||
label_pre_search.setStyleSheet("background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
|
||||
self.toolbar_search.addWidget(label_pre_search)
|
||||
|
||||
self.input_search = QLineEdit()
|
||||
self.input_search.setFrame(False)
|
||||
self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder']+"...")
|
||||
self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip'])
|
||||
self.input_search.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}")
|
||||
self.input_search.returnPressed.connect(self.search)
|
||||
self.toolbar_search.addWidget(self.input_search)
|
||||
|
||||
label_pos_search = QLabel()
|
||||
label_pos_search.setPixmap(QPixmap(resource.get_path('img/search.svg')))
|
||||
label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;")
|
||||
self.toolbar_search.addWidget(label_pos_search)
|
||||
|
||||
spacer = QWidget()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar_search.addWidget(spacer)
|
||||
self.layout.addWidget(self.toolbar_search)
|
||||
|
||||
toolbar = QToolBar()
|
||||
|
||||
self.checkbox_only_apps = QCheckBox()
|
||||
self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps'])
|
||||
self.checkbox_only_apps.setChecked(True)
|
||||
self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps)
|
||||
|
||||
toolbar = QToolBar()
|
||||
toolbar.addWidget(self.checkbox_only_apps)
|
||||
|
||||
spacer = QWidget()
|
||||
@@ -86,7 +89,7 @@ class ManageWindow(QWidget):
|
||||
|
||||
self.label_status = QLabel()
|
||||
self.label_status.setText('')
|
||||
self.label_status.setStyleSheet("color: orange")
|
||||
self.label_status.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
|
||||
toolbar.addWidget(self.label_status)
|
||||
|
||||
spacer = QWidget()
|
||||
@@ -94,28 +97,22 @@ class ManageWindow(QWidget):
|
||||
toolbar.addWidget(spacer)
|
||||
|
||||
self.bt_refresh = QToolButton()
|
||||
self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip'])
|
||||
self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
|
||||
self.bt_refresh.clicked.connect(lambda: self.refresh(clear_output=True))
|
||||
toolbar.addWidget(self.bt_refresh)
|
||||
|
||||
self.bt_update = QToolButton()
|
||||
self.bt_update.setIcon(QIcon(resource.get_path('img/update_green.svg')))
|
||||
self.bt_update.setEnabled(False)
|
||||
self.bt_update.clicked.connect(self.update_selected)
|
||||
toolbar.addWidget(self.bt_update)
|
||||
self.bt_upgrade = QToolButton()
|
||||
self.bt_upgrade.setToolTip(locale_keys['manage_window.bt.upgrade.tooltip'])
|
||||
self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg')))
|
||||
self.bt_upgrade.setEnabled(False)
|
||||
self.bt_upgrade.clicked.connect(self.update_selected)
|
||||
toolbar.addWidget(self.bt_upgrade)
|
||||
|
||||
self.layout.addWidget(toolbar)
|
||||
|
||||
self.table_apps = QTableWidget()
|
||||
self.table_apps.setColumnCount(len(self.column_names))
|
||||
self.table_apps.setFocusPolicy(Qt.NoFocus)
|
||||
self.table_apps.setShowGrid(False)
|
||||
self.table_apps.verticalHeader().setVisible(False)
|
||||
self.table_apps.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.table_apps.setHorizontalHeaderLabels(self.column_names)
|
||||
self.table_apps.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
||||
self._change_table_headers_policy()
|
||||
self.table_apps = AppsTable(self)
|
||||
self.table_apps.change_headers_policy()
|
||||
|
||||
self.layout.addWidget(self.table_apps)
|
||||
|
||||
@@ -126,35 +123,66 @@ class ManageWindow(QWidget):
|
||||
self.textarea_output.setVisible(False)
|
||||
self.textarea_output.setReadOnly(True)
|
||||
|
||||
self.thread_update = UpdateSelectedApps(self.controller)
|
||||
self.thread_update = UpdateSelectedApps()
|
||||
self.thread_update.signal_output.connect(self._update_action_output)
|
||||
self.thread_update.signal_finished.connect(self._finish_update_selected)
|
||||
|
||||
self.thread_refresh = RefreshApps(self.controller)
|
||||
self.thread_refresh = RefreshApps(self.manager)
|
||||
self.thread_refresh.signal.connect(self._finish_refresh)
|
||||
|
||||
self.thread_uninstall = UninstallApp()
|
||||
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.locale_keys)
|
||||
self.thread_downgrade.signal_output.connect(self._update_action_output)
|
||||
self.thread_downgrade.signal_finished.connect(self._finish_downgrade)
|
||||
|
||||
self.thread_get_info = GetAppInfo()
|
||||
self.thread_get_info.signal_finished.connect(self._finish_get_info)
|
||||
|
||||
self.thread_get_history = GetAppHistory()
|
||||
self.thread_get_history.signal_finished.connect(self._finish_get_history)
|
||||
|
||||
self.thread_search = SearchApps(self.manager)
|
||||
self.thread_search.signal_finished.connect(self._finish_search)
|
||||
|
||||
self.thread_install = InstallApp()
|
||||
self.thread_install.signal_output.connect(self._update_action_output)
|
||||
self.thread_install.signal_finished.connect(self._finish_install)
|
||||
|
||||
self.thread_animate_progress = AnimateProgress()
|
||||
self.thread_animate_progress.signal_change.connect(self._update_progress)
|
||||
|
||||
self.toolbar_bottom = QToolBar()
|
||||
self.label_updates = QLabel('')
|
||||
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
|
||||
self.toolbar_bottom.addWidget(self.label_updates)
|
||||
|
||||
spacer = QWidget()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar_bottom.addWidget(spacer)
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setTextVisible(False)
|
||||
self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar)
|
||||
|
||||
spacer = QWidget()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar_bottom.addWidget(spacer)
|
||||
|
||||
self.label_flatpak = QLabel(self._get_flatpak_label())
|
||||
self.toolbar_bottom.addWidget(self.label_flatpak)
|
||||
|
||||
self.layout.addWidget(self.toolbar_bottom)
|
||||
|
||||
self.centralize()
|
||||
|
||||
def _change_table_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
|
||||
header_horizontal = self.table_apps.horizontalHeader()
|
||||
for i in range(0, len(self.column_names)):
|
||||
header_horizontal.setSectionResizeMode(i, policy)
|
||||
|
||||
def changeEvent(self, e: QEvent):
|
||||
|
||||
if isinstance(e, QWindowStateChangeEvent):
|
||||
self._change_table_headers_policy(QHeaderView.Stretch if self.isMaximized() else QHeaderView.ResizeToContents)
|
||||
policy = QHeaderView.Stretch if self.isMaximized() else QHeaderView.ResizeToContents
|
||||
self.table_apps.change_headers_policy(policy)
|
||||
|
||||
def closeEvent(self, event):
|
||||
|
||||
@@ -162,35 +190,19 @@ class ManageWindow(QWidget):
|
||||
event.ignore()
|
||||
self.hide()
|
||||
|
||||
def _load_icon(self, http_response):
|
||||
icon_url = http_response.url().toString()
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(http_response.readAll())
|
||||
icon = QIcon(pixmap)
|
||||
self.icon_cache[icon_url] = icon
|
||||
|
||||
for idx, app in enumerate(self.apps):
|
||||
if app['model']['icon'] == icon_url:
|
||||
self.table_apps.item(idx, 0).setIcon(icon)
|
||||
self.resize_and_center()
|
||||
break
|
||||
|
||||
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_()
|
||||
if not flatpak.is_installed():
|
||||
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:
|
||||
self.label_flatpak.setText(self._get_flatpak_label())
|
||||
|
||||
def _get_flatpak_label(self):
|
||||
return 'flatpak: ' + self.controller.get_version()
|
||||
return 'flatpak: ' + flatpak.get_version()
|
||||
|
||||
def _acquire_lock(self):
|
||||
|
||||
@@ -211,24 +223,52 @@ class ManageWindow(QWidget):
|
||||
|
||||
self.thread_lock.release()
|
||||
|
||||
def _hide_output(self):
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.hide()
|
||||
|
||||
def _show_output(self):
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.show()
|
||||
|
||||
def refresh(self, clear_output: bool = True):
|
||||
|
||||
if self._acquire_lock():
|
||||
self._check_flatpak_installed()
|
||||
self._begin_action(self.locale_keys['manage_window.status.refreshing'] + '...')
|
||||
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
|
||||
|
||||
if clear_output:
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.hide()
|
||||
self._hide_output()
|
||||
|
||||
self.thread_refresh.start()
|
||||
|
||||
def _finish_refresh(self):
|
||||
def _finish_refresh(self, apps: List[dict]):
|
||||
|
||||
self.update_apps(self.thread_refresh.apps)
|
||||
self.update_apps(apps)
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
|
||||
def uninstall_app(self, app_ref: str):
|
||||
self._check_flatpak_installed()
|
||||
|
||||
if self._acquire_lock():
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.setVisible(True)
|
||||
self._begin_action(self.locale_keys['manage_window.status.uninstalling'])
|
||||
|
||||
self.thread_uninstall.app_ref = app_ref
|
||||
self.thread_uninstall.start()
|
||||
|
||||
def _finish_uninstall(self):
|
||||
self.finish_action()
|
||||
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:
|
||||
@@ -240,8 +280,8 @@ class ManageWindow(QWidget):
|
||||
app['visible'] = not hidden
|
||||
|
||||
self.change_update_state()
|
||||
self._change_table_headers_policy(QHeaderView.Stretch)
|
||||
self._change_table_headers_policy()
|
||||
self.table_apps.change_headers_policy(QHeaderView.Stretch)
|
||||
self.table_apps.change_headers_policy()
|
||||
self.resize_and_center()
|
||||
|
||||
def change_update_state(self):
|
||||
@@ -259,7 +299,10 @@ class ManageWindow(QWidget):
|
||||
|
||||
total_updates = app_updates + runtime_updates
|
||||
if total_updates > 0:
|
||||
self.label_updates.setText('{}: {} ( {} apps | {} runtimes )'.format(self.locale_keys['manage_window.label.updates'], total_updates, app_updates, runtime_updates))
|
||||
self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates))
|
||||
self.label_updates.setToolTip('{} {} | {} runtimes'.format(app_updates,
|
||||
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
|
||||
runtime_updates))
|
||||
else:
|
||||
self.label_updates.setText('')
|
||||
|
||||
@@ -268,9 +311,8 @@ class ManageWindow(QWidget):
|
||||
enable_bt_update = True
|
||||
break
|
||||
|
||||
self.bt_update.setEnabled(enable_bt_update)
|
||||
|
||||
self.tray_icon.notify_updates(total_updates)
|
||||
self.bt_upgrade.setEnabled(enable_bt_update)
|
||||
self.tray_icon.notify_updates([app['model'] for app in self.apps if app['model']['update']])
|
||||
|
||||
def centralize(self):
|
||||
geo = self.frameGeometry()
|
||||
@@ -282,79 +324,33 @@ class ManageWindow(QWidget):
|
||||
def update_apps(self, apps: List[dict]):
|
||||
self._check_flatpak_installed()
|
||||
|
||||
self.table_apps.setEnabled(True)
|
||||
self.apps = []
|
||||
|
||||
self.table_apps.setRowCount(len(apps) if apps else 0)
|
||||
napps = 0 # number of apps (not runtimes)
|
||||
|
||||
if apps:
|
||||
for idx, app in enumerate(apps):
|
||||
|
||||
col_name = QTableWidgetItem()
|
||||
col_name.setText(app['name'])
|
||||
col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
|
||||
if not app['icon']:
|
||||
col_name.setIcon(self.icon_flathub)
|
||||
else:
|
||||
cached_icon = self.icon_cache.get(app['icon'])
|
||||
|
||||
if cached_icon:
|
||||
col_name.setIcon(cached_icon)
|
||||
else:
|
||||
col_name.setIcon(self.icon_flathub)
|
||||
self.network_man.get(QNetworkRequest(QUrl(app['icon'])))
|
||||
|
||||
self.table_apps.setItem(idx, 0, col_name)
|
||||
|
||||
col_version = QTableWidgetItem()
|
||||
col_version.setText(app['version'])
|
||||
col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 1, col_version)
|
||||
|
||||
col_release = QTableWidgetItem()
|
||||
col_release.setText(app['latest_version'])
|
||||
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 2, col_release)
|
||||
|
||||
if app['update']:
|
||||
col_release.setForeground(QColor('orange'))
|
||||
|
||||
col_branch = QTableWidgetItem()
|
||||
col_branch.setText(app['branch'])
|
||||
col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 3, col_branch)
|
||||
|
||||
col_arch = QTableWidgetItem()
|
||||
col_arch.setText(app['arch'])
|
||||
col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 4, col_arch)
|
||||
|
||||
col_package = QTableWidgetItem()
|
||||
col_package.setText(app['ref'])
|
||||
col_package.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 5, col_package)
|
||||
|
||||
col_origin = QTableWidgetItem()
|
||||
col_origin.setText(app['origin'])
|
||||
col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self.table_apps.setItem(idx, 6, col_origin)
|
||||
|
||||
for app in apps:
|
||||
app_model = {'model': app,
|
||||
'update_checked': app['update'],
|
||||
'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()}
|
||||
|
||||
col_update = UpdateToggleButton(app_model, self, app['update']) if app['update'] else None
|
||||
self.table_apps.setCellWidget(idx, 7, col_update)
|
||||
|
||||
napps += 1 if not app['runtime'] else 0
|
||||
self.apps.append(app_model)
|
||||
|
||||
if napps == 0:
|
||||
self.checkbox_only_apps.setChecked(False)
|
||||
self.checkbox_only_apps.setCheckable(False)
|
||||
else:
|
||||
self.checkbox_only_apps.setCheckable(True)
|
||||
self.checkbox_only_apps.setChecked(True)
|
||||
|
||||
self.table_apps.update_apps(self.apps)
|
||||
self.change_update_state()
|
||||
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
|
||||
self.resize_and_center()
|
||||
|
||||
def resize_and_center(self):
|
||||
new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.column_names))]) * 1.05
|
||||
new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05
|
||||
self.resize(new_width, self.height())
|
||||
self.centralize()
|
||||
|
||||
@@ -369,7 +365,7 @@ class ManageWindow(QWidget):
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.setVisible(True)
|
||||
|
||||
self._begin_action(self.locale_keys['manage_window.status.updating'] + '...')
|
||||
self._begin_action(self.locale_keys['manage_window.status.upgrading'])
|
||||
self.thread_update.refs_to_update = to_update
|
||||
self.thread_update.start()
|
||||
|
||||
@@ -382,50 +378,121 @@ class ManageWindow(QWidget):
|
||||
self.textarea_output.appendPlainText(output)
|
||||
|
||||
def _begin_action(self, action_label: str):
|
||||
self.label_status.setText(action_label)
|
||||
self.bt_update.setEnabled(False)
|
||||
self.thread_animate_progress.stop = False
|
||||
self.thread_animate_progress.start()
|
||||
self.ref_progress_bar.setVisible(True)
|
||||
self.progress_bar.setValue(50)
|
||||
self.label_status.setText(action_label + "...")
|
||||
self.toolbar_search.setVisible(False)
|
||||
self.bt_upgrade.setEnabled(False)
|
||||
self.bt_refresh.setEnabled(False)
|
||||
self.checkbox_only_apps.setEnabled(False)
|
||||
self.table_apps.setEnabled(False)
|
||||
|
||||
def finish_action(self):
|
||||
def finish_action(self, clear_search: bool = True):
|
||||
self.thread_animate_progress.stop = True
|
||||
self.ref_progress_bar.setVisible(False)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setVisible(False)
|
||||
self.bt_refresh.setEnabled(True)
|
||||
self.toolbar_search.setVisible(True)
|
||||
self.checkbox_only_apps.setEnabled(True)
|
||||
self.table_apps.setEnabled(True)
|
||||
self.input_search.setEnabled(True)
|
||||
self.label_status.setText('')
|
||||
|
||||
if clear_search:
|
||||
self.input_search.setText('')
|
||||
|
||||
# Threaded actions
|
||||
class UpdateSelectedApps(QThread):
|
||||
def downgrade_app(self, app: dict):
|
||||
|
||||
signal_finished = pyqtSignal()
|
||||
signal_output = pyqtSignal(str)
|
||||
self._check_flatpak_installed()
|
||||
|
||||
def __init__(self, controller: FlatpakController):
|
||||
super(UpdateSelectedApps, self).__init__()
|
||||
self.controller = controller
|
||||
self.refs_to_update = []
|
||||
if self._acquire_lock():
|
||||
|
||||
def run(self):
|
||||
pwd = None
|
||||
|
||||
for app_ref in self.refs_to_update:
|
||||
for output in self.controller.update(app_ref):
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
if not is_root():
|
||||
pwd, ok = ask_root_password(self.locale_keys)
|
||||
|
||||
self.signal_finished.emit()
|
||||
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'])
|
||||
|
||||
class RefreshApps(QThread):
|
||||
self.thread_downgrade.app = app
|
||||
self.thread_downgrade.root_password = pwd
|
||||
self.thread_downgrade.start()
|
||||
|
||||
signal = pyqtSignal()
|
||||
def get_app_info(self, app: dict):
|
||||
|
||||
def __init__(self, controller: FlatpakController):
|
||||
super(RefreshApps, self).__init__()
|
||||
self.controller = controller
|
||||
self.apps = None
|
||||
if self._acquire_lock():
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.setVisible(False)
|
||||
self._begin_action(self.locale_keys['manage_window.status.info'])
|
||||
|
||||
def run(self):
|
||||
self.apps = self.controller.refresh()
|
||||
self.signal.emit()
|
||||
self.thread_get_info.app = app
|
||||
self.thread_get_info.start()
|
||||
|
||||
def get_app_history(self, app: dict):
|
||||
if self._acquire_lock():
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.setVisible(False)
|
||||
self._begin_action(self.locale_keys['manage_window.status.history'])
|
||||
|
||||
self.thread_get_history.app = app
|
||||
self.thread_get_history.start()
|
||||
|
||||
def _finish_get_info(self, app_info: dict):
|
||||
self._release_lock()
|
||||
self.finish_action()
|
||||
self.change_update_state()
|
||||
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys)
|
||||
dialog_info.exec_()
|
||||
|
||||
def _finish_get_history(self, app: dict):
|
||||
self._release_lock()
|
||||
self.finish_action()
|
||||
self.change_update_state()
|
||||
dialog_history = HistoryDialog(app, self.table_apps.get_selected_app_icon(), self.locale_keys)
|
||||
dialog_history.exec_()
|
||||
|
||||
def search(self):
|
||||
|
||||
word = self.input_search.text().strip()
|
||||
|
||||
if word and self._acquire_lock():
|
||||
self.textarea_output.clear()
|
||||
self.textarea_output.setVisible(False)
|
||||
self._begin_action(self.locale_keys['manage_window.status.searching'])
|
||||
self.thread_search.word = word
|
||||
self.thread_search.start()
|
||||
|
||||
def _finish_search(self, apps_found: List[dict]):
|
||||
|
||||
self._release_lock()
|
||||
self.finish_action(clear_search=False)
|
||||
self.update_apps(apps_found)
|
||||
|
||||
def install_app(self, app: dict):
|
||||
|
||||
self._check_flatpak_installed()
|
||||
|
||||
if self._acquire_lock():
|
||||
self._begin_action(self.locale_keys['manage_window.status.installing'])
|
||||
self._show_output()
|
||||
|
||||
self.thread_install.app = app
|
||||
self.thread_install.start()
|
||||
|
||||
def _finish_install(self):
|
||||
self.input_search.setText('')
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
self.refresh(clear_output=False)
|
||||
|
||||
def _update_progress(self, value: int):
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
Reference in New Issue
Block a user