search and install

This commit is contained in:
Vinícius Moreira
2019-06-28 19:01:37 -03:00
committed by GitHub
parent e3a8df5dab
commit 22eed7af67
16 changed files with 649 additions and 83 deletions

View File

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

View File

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

View File

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

View File

@@ -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'])

View File

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

View File

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

120
fpakman/resources/img/install.svg Executable file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="48"
height="48"
viewBox="0 0 48 47.999999"
xml:space="preserve"
sodipodi:docname="install.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata135"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs133" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="739"
id="namedview131"
showgrid="false"
showborder="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="0.67615769"
inkscape:cx="286.91482"
inkscape:cy="174.51451"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g98"
transform="scale(0.137524)"
style="fill:#00ff00">
<path
d="m 349.03,141.226 v 66.579 c 0,5.012 -4.061,9.079 -9.079,9.079 H 216.884 v 123.067 c 0,5.019 -4.067,9.079 -9.079,9.079 h -66.579 c -5.009,0 -9.079,-4.061 -9.079,-9.079 V 216.884 H 9.079 C 4.063,216.884 0,212.817 0,207.805 v -66.579 c 0,-5.013 4.063,-9.079 9.079,-9.079 H 132.147 V 9.079 C 132.147,4.061 136.216,0 141.226,0 h 66.579 c 5.012,0 9.079,4.061 9.079,9.079 v 123.068 h 123.067 c 5.019,0 9.079,4.066 9.079,9.079 z"
id="path96"
inkscape:connector-curvature="0"
style="fill:#00ff00" />
</g>
<g
id="g100"
transform="translate(0,-301.03)">
</g>
<g
id="g102"
transform="translate(0,-301.03)">
</g>
<g
id="g104"
transform="translate(0,-301.03)">
</g>
<g
id="g106"
transform="translate(0,-301.03)">
</g>
<g
id="g108"
transform="translate(0,-301.03)">
</g>
<g
id="g110"
transform="translate(0,-301.03)">
</g>
<g
id="g112"
transform="translate(0,-301.03)">
</g>
<g
id="g114"
transform="translate(0,-301.03)">
</g>
<g
id="g116"
transform="translate(0,-301.03)">
</g>
<g
id="g118"
transform="translate(0,-301.03)">
</g>
<g
id="g120"
transform="translate(0,-301.03)">
</g>
<g
id="g122"
transform="translate(0,-301.03)">
</g>
<g
id="g124"
transform="translate(0,-301.03)">
</g>
<g
id="g126"
transform="translate(0,-301.03)">
</g>
<g
id="g128"
transform="translate(0,-301.03)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

120
fpakman/resources/img/search.svg Executable file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 12 12"
xml:space="preserve"
sodipodi:docname="search.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
width="12"
height="12"><metadata
id="metadata126"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs124" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="739"
id="namedview122"
showgrid="false"
showborder="false"
inkscape:zoom="0.53133476"
inkscape:cx="834.65593"
inkscape:cy="-101.88082"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="Search"
transform="scale(0.04794017)"
style="fill:#999999">
<path
style="clip-rule:evenodd;fill:#999999;fill-rule:evenodd"
d="m 244.186,214.604 -54.379,-54.378 c -0.289,-0.289 -0.628,-0.491 -0.93,-0.76 10.7,-16.231 16.945,-35.66 16.945,-56.554 C 205.822,46.075 159.747,0 102.911,0 46.075,0 0,46.075 0,102.911 c 0,56.835 46.074,102.911 102.91,102.911 20.895,0 40.323,-6.245 56.554,-16.945 0.269,0.301 0.47,0.64 0.759,0.929 l 54.38,54.38 c 8.169,8.168 21.413,8.168 29.583,0 8.168,-8.169 8.168,-21.413 0,-29.582 z M 102.911,170.146 c -37.134,0 -67.236,-30.102 -67.236,-67.235 0,-37.134 30.103,-67.236 67.236,-67.236 37.132,0 67.235,30.103 67.235,67.236 0,37.133 -30.103,67.235 -67.235,67.235 z"
id="path88"
inkscape:connector-curvature="0" />
</g>
<g
id="g91"
transform="translate(0,-238.312)">
</g>
<g
id="g93"
transform="translate(0,-238.312)">
</g>
<g
id="g95"
transform="translate(0,-238.312)">
</g>
<g
id="g97"
transform="translate(0,-238.312)">
</g>
<g
id="g99"
transform="translate(0,-238.312)">
</g>
<g
id="g101"
transform="translate(0,-238.312)">
</g>
<g
id="g103"
transform="translate(0,-238.312)">
</g>
<g
id="g105"
transform="translate(0,-238.312)">
</g>
<g
id="g107"
transform="translate(0,-238.312)">
</g>
<g
id="g109"
transform="translate(0,-238.312)">
</g>
<g
id="g111"
transform="translate(0,-238.312)">
</g>
<g
id="g113"
transform="translate(0,-238.312)">
</g>
<g
id="g115"
transform="translate(0,-238.312)">
</g>
<g
id="g117"
transform="translate(0,-238.312)">
</g>
<g
id="g119"
transform="translate(0,-238.312)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -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
about.info.rate=If this tool is useful for you, give it a star on Github to keep it up
yes=yes
no=no

View File

@@ -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
about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla
yes=sí
no=no

View File

@@ -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é
about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé
yes=sim
no=não

View File

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

View File

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

View File

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

View File

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

View File

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