diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b4dbb5..0613a9b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.3.0] - 2019-06-XX ### Features -- Allows to retrieve app information and history by right clicking it. -- Allows app uninstall and downgrade by right clicking it. +- Applications search +- Now when you right-click a selected application you can: + - retrieve its information + - retrieve its commit history + - downgrade + - install it - "About" window available when right-clicking the tray icon. ### Improvements @@ -18,7 +22,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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. -- Retrieving only the installed applications data from the origin API. - Minor GUI improvements ## [0.2.1] - 2019-06-24 diff --git a/fpakman/app.py b/fpakman/app.py index bac75a8a..081e0460 100755 --- a/fpakman/app.py +++ b/fpakman/app.py @@ -25,6 +25,7 @@ manager = FlatpakManager(cache_expire=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', trayIcon = TrayIcon(locale_keys=locale_keys, manager=manager, check_interval=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60))) +trayIcon.load_database() trayIcon.show() sys.exit(app.exec_()) diff --git a/fpakman/core/controller.py b/fpakman/core/controller.py index d8ebddbb..373e77d3 100755 --- a/fpakman/core/controller.py +++ b/fpakman/core/controller.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from threading import Lock, Thread +from threading import Lock from typing import List import requests @@ -19,17 +19,12 @@ class FlatpakManager: self.lock_db_read = Lock() self.lock_read = Lock() - # TODO remove if not necessary for future releases - def load_full_database_async(self): - Thread(target=self.load_full_database, daemon=True).start() - - # TODO remove if not necessary for future releases def load_full_database(self): self.lock_db_read.acquire() try: - res = self.http_session.get(__FLATHUB_API_URL__ + '/apps') + res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30) if res.status_code == 200: for app in res.json(): @@ -40,7 +35,7 @@ class FlatpakManager: def _request_app_data(self, app_id: str): try: - res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=60) + res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30) if res.status_code == 200: return res.json() @@ -50,6 +45,58 @@ class FlatpakManager: print("Could not retrieve app data for id '{}'. Timeout".format(app_id)) return None + def _fill_api_data(self, app: dict): + + api_data = self.cache_apps.get(app['id']) + + if (not app['runtime'] and not api_data) or (api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()): # if api data is not cached or expired, tries to retrieve it + api_data = self._request_app_data(app['id']) + + if api_data: + api_data['expires_at'] = datetime.utcnow() + timedelta(seconds=self.cache_expire) + self.cache_apps[app['id']] = api_data + + if not api_data: + for attr in ('latest_version', 'icon', 'description'): + if attr not in app: + app[attr] = None + else: + app['latest_version'] = api_data.get('currentReleaseVersion') + app['icon'] = api_data.get('iconMobileUrl') + + for attr in ('name', 'description'): + if not app.get(attr): + app[attr] = api_data.get(attr) + + if app['icon'].startswith('/'): + app['icon'] = __FLATHUB_URL__ + app['icon'] + + def search(self, word: str) -> List[dict]: + + res = [] + apps_found = flatpak.search(word) + + if apps_found: + + already_read = set() + installed_apps = self.read_installed() + + if installed_apps: + for app in apps_found: + for installed_app in installed_apps: + if app['id'] == installed_app['id']: + res.append(installed_app) + already_read.add(app['id']) + + for app in apps_found: + if app['id'] not in already_read: + app['update'] = False + app['installed'] = False + self._fill_api_data(app) + res.append(app) + + return res + def read_installed(self) -> List[dict]: self.lock_read.acquire() @@ -63,25 +110,9 @@ class FlatpakManager: available_updates = flatpak.list_updates_as_str() for app in installed: - - app_data = self.cache_apps.get(app['id']) - - if (not app['runtime'] and not app_data) or (app_data and app_data['expires_at'] <= datetime.utcnow()): # if data is not cached or expired, tries to retrieve it - app_data = self._request_app_data(app['id']) - app_data['expires_at'] = datetime.utcnow() + timedelta(seconds=self.cache_expire) - self.cache_apps[app['id']] = app_data - - if not app_data: - app['latest_version'] = None - app['icon'] = None - else: - app['latest_version'] = app_data['currentReleaseVersion'] - app['icon'] = app_data['iconMobileUrl'] - - if app['icon'].startswith('/'): - app['icon'] = __FLATHUB_URL__ + app['icon'] - + self._fill_api_data(app) app['update'] = app['id'] in available_updates + app['installed'] = True return installed diff --git a/fpakman/core/flatpak.py b/fpakman/core/flatpak.py index 2c725a29..5da0358b 100755 --- a/fpakman/core/flatpak.py +++ b/fpakman/core/flatpak.py @@ -147,3 +147,67 @@ def get_app_commits_data(app_ref: str, origin: str) -> List[dict]: commit = {} return commits + + +def search(word: str) -> List[dict]: + cli_version = get_version() + + res = system.run_cmd('{} search {}'.format(BASE_CMD, word)) + + found = [] + + split_res = res.split('\n') + + if split_res and split_res[0].lower() != 'no matches found': + for info in split_res: + if info: + info_list = info.split('\t') + + if cli_version >= '1.3.0': + version = info_list[3].strip() + found.append({ + 'name': info_list[0].strip(), + 'description': info_list[1].strip(), + 'id': info_list[2].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[4].strip(), + 'origin': info_list[5].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + elif cli_version >= '1.2.0': + desc = info_list[0].split('-') + version = info_list[2].strip() + found.append({ + 'name': desc[0].strip(), + 'description': desc[1].strip(), + 'id': info_list[1].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[3].strip(), + 'origin': info_list[4].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + else: + version = info_list[1].strip() + found.append({ + 'name': '', + 'description': info_list[4].strip(), + 'id': info_list[0].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[2].strip(), + 'origin': info_list[3].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + return found + + +def install_and_stream(app_id: str, origin: str): + return system.stream_cmd([BASE_CMD, 'install', origin, app_id, '-y']) diff --git a/fpakman/core/system.py b/fpakman/core/system.py index a7b1715b..937a27bb 100644 --- a/fpakman/core/system.py +++ b/fpakman/core/system.py @@ -1,6 +1,9 @@ +import os import subprocess from typing import List +from fpakman.core import resource + def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False) -> str: res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, env={'LANG': 'en'}) @@ -9,3 +12,8 @@ def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False) def stream_cmd(cmd: List[str]): return subprocess.Popen(cmd, stdout=subprocess.PIPE, env={'LANG': 'en'}).stdout + + +def notify_user(msg: str, icon_path: str = resource.get_path('img/flathub_45.svg')): + if bool(os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1)): + os.system("notify-send {} '{}'".format("-i {}".format(icon_path) if icon_path else '', msg)) diff --git a/fpakman/core/util.py b/fpakman/core/util.py index 4a6afda1..a83daf1b 100644 --- a/fpakman/core/util.py +++ b/fpakman/core/util.py @@ -2,6 +2,9 @@ import locale from fpakman.core import resource import glob +import re + +HTML_RE = re.compile(r'<[^>]+>') def get_locale_keys(key: str = None): @@ -38,3 +41,7 @@ def get_locale_keys(key: str = None): locale_obj[keyval[0].strip()] = keyval[1].strip() return locale_obj + + +def strip_html(string: str): + return HTML_RE.sub('', string) diff --git a/fpakman/resources/img/install.svg b/fpakman/resources/img/install.svg new file mode 100755 index 00000000..8f43edb1 --- /dev/null +++ b/fpakman/resources/img/install.svg @@ -0,0 +1,120 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/search.svg b/fpakman/resources/img/search.svg new file mode 100755 index 00000000..1519d2f8 --- /dev/null +++ b/fpakman/resources/img/search.svg @@ -0,0 +1,120 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/locale/en b/fpakman/resources/locale/en index fc2de6c2..353c87e2 100644 --- a/fpakman/resources/locale/en +++ b/fpakman/resources/locale/en @@ -1,5 +1,6 @@ manage_window.title=Flatpak Application Manager manage_window.columns.latest_version=Latest Version +manage_window.columns.installed=Installed manage_window.columns.update=Upgrade ? manage_window.apps_table.row.actions.info=Information manage_window.apps_table.row.actions.history=History @@ -8,8 +9,11 @@ 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.apps_table.row.actions.install=Install manage_window.apps_table.upgrade_toggle.tooltip=Click it to check or uncheck the update manage_window.checkbox.only_apps=Apps +window_manage.input_search.placeholder=Search +window_manage.input_search.tooltip=Type and press ENTER to search for applications manage_window.label.updates=Updates manage_window.status.refreshing=Refreshing manage_window.status.upgrading=Upgrading @@ -17,6 +21,8 @@ manage_window.status.uninstalling=Uninstalling manage_window.status.downgrading=Downgrading manage_window.status.info=Retrieving information manage_window.status.history=Retrieving history +manage_window.status.searching=Searching +manage_window.status.installing=Installing 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 @@ -35,12 +41,14 @@ popup.button.cancel=Cancel tray.action.manage=Manage applications tray.action.exit=Exit tray.action.about=About +tray.action.refreshing=Refreshing notification.new_updates=Updates found flatpak.info.arch=arch flatpak.info.branch=branch flatpak.info.collection=collection flatpak.info.commit=commit flatpak.info.date=date +flatpak.info.description=description flatpak.info.id=id flatpak.info.installation=installation flatpak.info.installed=installed @@ -57,4 +65,6 @@ flatpak.info.version=version about.info.desc=Non-official Flatpak application management graphical interface about.info.link=More information at about.info.license=Free license -about.info.rate=If this tool is useful for you, give it a star on Github to keep it up \ No newline at end of file +about.info.rate=If this tool is useful for you, give it a star on Github to keep it up +yes=yes +no=no \ No newline at end of file diff --git a/fpakman/resources/locale/es b/fpakman/resources/locale/es index 65a35dba..1fac6233 100644 --- a/fpakman/resources/locale/es +++ b/fpakman/resources/locale/es @@ -1,6 +1,7 @@ manage_window.title=Administrador de Aplicativos Flatpak manage_window.columns.latest_version=Ultima Versión manage_window.columns.update=Actualizar ? +manage_window.columns.installed=Instalado manage_window.apps_table.row.actions.info=Información manage_window.apps_table.row.actions.history=Historia manage_window.apps_table.row.actions.uninstall=Desinstalar @@ -8,8 +9,11 @@ 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.apps_table.row.actions.install=Instalar manage_window.apps_table.upgrade_toggle.tooltip=Haz clic para marcar o desmarcar la actualización manage_window.checkbox.only_apps=Aplicativos +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Escriba y oprima ENTER para buscar aplicativos manage_window.label.updates=Actualizaciones manage_window.status.refreshing=Recargando manage_window.status.upgrading=Actualizando @@ -17,6 +21,8 @@ manage_window.status.uninstalling=Desinstalando manage_window.status.downgrading=Revirtiendo manage_window.status.info=Obteniendo información manage_window.status.history=Obteniendo la historia +manage_window.status.searching=Buscando +manage_window.status.installing=Instalando 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 @@ -35,12 +41,14 @@ popup.button.cancel=Cancelar tray.action.manage=Administrar aplicativos tray.action.exit=Salir tray.action.about=Sobre +tray.action.refreshing=Recargando notification.new_updates=Actualizaciones encontradas flatpak.info.arch=arquitectura flatpak.info.branch=rama flatpak.info.collection=colección flatpak.info.commit=commit flatpak.info.date=fecha +flatpak.info.description=descripción flatpak.info.id=id flatpak.info.installation=instalación flatpak.info.installed=instalado @@ -57,4 +65,6 @@ flatpak.info.version=versión about.info.desc=Interfaz grafica no oficial para administración de aplicativos Flatpak about.info.link=Mas informaciones en about.info.license=Licencia gratuita -about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla \ No newline at end of file +about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla +yes=sí +no=no \ No newline at end of file diff --git a/fpakman/resources/locale/pt b/fpakman/resources/locale/pt index 6622e9ed..776dd22c 100644 --- a/fpakman/resources/locale/pt +++ b/fpakman/resources/locale/pt @@ -1,6 +1,7 @@ manage_window.title=Gerenciador de Aplicativos Flatpak manage_window.columns.latest_version=Última Versão manage_window.columns.update=Atualizar ? +manage_window.columns.installed=Instalado manage_window.apps_table.row.actions.info=Informação manage_window.apps_table.row.actions.history=Histórico manage_window.apps_table.row.actions.uninstall=Desinstalar @@ -8,8 +9,11 @@ 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.apps_table.row.actions.install=Instalar manage_window.apps_table.upgrade_toggle.tooltip=Clique para marcar ou desmarcar a atualização manage_window.checkbox.only_apps=Aplicativos +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos manage_window.label.updates=Atualizações manage_window.status.refreshing=Recarregando manage_window.status.upgrading=Atualizando @@ -17,6 +21,8 @@ manage_window.status.uninstalling=Desinstalando manage_window.status.downgrading=Revertendo manage_window.status.info=Obtendo informação manage_window.status.history=Obtendo histórico +manage_window.status.searching=Buscando +manage_window.status.installing=Instalando 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 @@ -35,12 +41,14 @@ popup.button.cancel=Cancelar tray.action.manage=Gerenciar aplicativos tray.action.exit=Sair tray.action.about=Sobre +tray.action.refreshing=Recarregando notification.new_updates=Atualizações encontradas flatpak.info.arch=arquitetura flatpak.info.branch=ramo flatpak.info.collection=coleção flatpak.info.commit=commit flatpak.info.date=data +flatpak.info.description=descrição flatpak.info.id=id flatpak.info.installation=instalação flatpak.info.installed=instalado @@ -57,4 +65,6 @@ flatpak.info.version=versão about.info.desc=Interface gráfica não oficial para gerenciamento de aplicativos Flatpak about.info.link=Mais informações em about.info.license=Licença gratuita -about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé \ No newline at end of file +about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé +yes=sim +no=não \ No newline at end of file diff --git a/fpakman/view/qt/apps_table.py b/fpakman/view/qt/apps_table.py index b8e37748..b2696601 100644 --- a/fpakman/view/qt/apps_table.py +++ b/fpakman/view/qt/apps_table.py @@ -6,7 +6,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ QHeaderView -from fpakman.core import resource +from fpakman.core import resource, util from fpakman.view.qt import dialog @@ -44,8 +44,9 @@ class AppsTable(QTableWidget): 'manage_window.columns.latest_version', 'flatpak.info.branch', 'flatpak.info.arch', - 'flatpak.info.ref', + 'flatpak.info.id', 'flatpak.info.origin', + 'manage_window.columns.installed', 'manage_window.columns.update']] self.setColumnCount(len(self.column_names)) self.setFocusPolicy(Qt.NoFocus) @@ -62,33 +63,40 @@ class AppsTable(QTableWidget): self.icon_cache = {} def contextMenuEvent(self, QContextMenuEvent): # selected row right click event - napps = len([app for app in self.window.apps if not app['model']['runtime']]) - - menu_row = QMenu() - - 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) app = self.get_selected_app() - 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) - action_uninstall.setEnabled(not napps or not app['model']['runtime']) # only enabled for runtimes when no apps are available - menu_row.addAction(action_uninstall) + menu_row = QMenu() - 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) + if app['model']['installed']: + napps = len([app for app in self.window.apps if not app['model']['runtime']]) + + 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) + action_uninstall.setEnabled(not napps or not app['model']['runtime']) # only enabled for runtimes when no apps are available + 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()) @@ -122,6 +130,9 @@ class AppsTable(QTableWidget): 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() @@ -142,9 +153,12 @@ class AppsTable(QTableWidget): 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) @@ -162,11 +176,13 @@ class AppsTable(QTableWidget): 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']: @@ -175,25 +191,38 @@ class AppsTable(QTableWidget): 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_package = QTableWidgetItem() - col_package.setText(app['model']['ref']) - col_package.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.setItem(idx, 5, col_package) + 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 = QTableWidgetItem() + col_installed.setText(self.parent().locale_keys['yes'] if app['model']['installed'] else self.parent().locale_keys['no']) + col_installed.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_installed.setTextAlignment(Qt.AlignCenter) + col_installed.setToolTip(tooltip) + col_installed.setForeground(Qt.blue if app['model']['installed'] else Qt.red) + + self.setItem(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, 7, col_update) + self.setCellWidget(idx, 8, col_update) def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): header_horizontal = self.horizontalHeader() diff --git a/fpakman/view/qt/info.py b/fpakman/view/qt/info.py index db32fb30..0ee09ecf 100644 --- a/fpakman/view/qt/info.py +++ b/fpakman/view/qt/info.py @@ -1,6 +1,6 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \ - QLineEdit, QLabel + QLineEdit, QLabel, QPlainTextEdit class InfoDialog(QDialog): @@ -19,12 +19,17 @@ class InfoDialog(QDialog): for attr in sorted(app.keys()): - if attr != 'name': - text = QLineEdit() - text.setText(app[attr]) - text.setStyleSheet("width: 400px") + 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) - text.setCursorPosition(0) label = QLabel("{}: ".format(locale_keys.get('flatpak.info.' + attr, attr)).capitalize()) label.setStyleSheet("font-weight: bold") diff --git a/fpakman/view/qt/systray.py b/fpakman/view/qt/systray.py index b6e6601b..7fba210e 100755 --- a/fpakman/view/qt/systray.py +++ b/fpakman/view/qt/systray.py @@ -6,7 +6,7 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QSystemTrayIcon, QMenu from fpakman.core.controller import FlatpakManager -from fpakman.core import resource +from fpakman.core import resource, system from fpakman.view.qt.about import AboutDialog from fpakman.view.qt.window import ManageWindow @@ -33,6 +33,19 @@ class UpdateCheck(QThread): 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, manager: FlatpakManager, check_interval: int = 60): @@ -46,8 +59,12 @@ class TrayIcon(QSystemTrayIcon): 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) @@ -64,6 +81,16 @@ class TrayIcon(QSystemTrayIcon): self.dialog_about = AboutDialog(self.locale_keys) + self.thread_database = LoadDatabase(manager) + self.thread_database.signal_finished.connect(self._update_menu) + + 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: int): if updates > 0: if self.icon().cacheKey() != self.icon_update.cacheKey(): @@ -72,11 +99,7 @@ class TrayIcon(QSystemTrayIcon): msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], 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() + system.notify_user(msg) else: self.setIcon(self.icon_default) diff --git a/fpakman/view/qt/thread.py b/fpakman/view/qt/thread.py index c00ccd5c..f21fef81 100644 --- a/fpakman/view/qt/thread.py +++ b/fpakman/view/qt/thread.py @@ -97,6 +97,7 @@ class GetAppInfo(QThread): 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 @@ -113,3 +114,42 @@ class GetAppHistory(QThread): 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() diff --git a/fpakman/view/qt/window.py b/fpakman/view/qt/window.py index c21f8b07..feb0de3c 100755 --- a/fpakman/view/qt/window.py +++ b/fpakman/view/qt/window.py @@ -4,9 +4,9 @@ from threading import Lock from typing import List from PyQt5.QtCore import QEvent -from PyQt5.QtGui import QIcon, QWindowStateChangeEvent +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ - QSizePolicy, QLabel, QPlainTextEdit + QSizePolicy, QLabel, QPlainTextEdit, QLineEdit from fpakman.core import resource, flatpak from fpakman.core.controller import FlatpakManager @@ -16,7 +16,7 @@ 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 + GetAppHistory, SearchApps, InstallApp class ManageWindow(QWidget): @@ -42,12 +42,43 @@ class ManageWindow(QWidget): 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() @@ -56,7 +87,7 @@ class ManageWindow(QWidget): self.label_status = QLabel() self.label_status.setText('') - self.label_status.setStyleSheet("color: orange") + self.label_status.setStyleSheet("color: orange; font-weight: bold") toolbar.addWidget(self.label_status) spacer = QWidget() @@ -111,6 +142,13 @@ class ManageWindow(QWidget): 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.toolbar_bottom = QToolBar() self.label_updates = QLabel('') self.label_updates.setStyleSheet("color: red;") @@ -170,6 +208,14 @@ 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(): @@ -177,8 +223,7 @@ class ManageWindow(QWidget): 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() @@ -321,17 +366,23 @@ class ManageWindow(QWidget): def _begin_action(self, action_label: str): 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.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('') + def downgrade_app(self, app: dict): self._check_flatpak_installed() @@ -387,3 +438,37 @@ class ManageWindow(QWidget): 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)