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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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)