improving initialization time and reducing async overload

This commit is contained in:
Vinícius Moreira
2019-07-08 12:15:15 -03:00
committed by GitHub
parent 735e590756
commit 74fa85a2bb
18 changed files with 577 additions and 240 deletions

View File

@@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.3.1] ## [0.3.1]
### Improvements ### Improvements
- Console output now is optional and now shown by default. - Console output now is optional and now shown by default.
- Full Flatpak database is not loaded during initialization (management panel is quickly available for the user)
- Applications data that must be retrieved from Flathub API are now retrieved on demand and cached
- Cache cleaners (for icons and API data) to improve memory usage.
- New environment variable and argument: FPAKMAN_ICON_EXPIRATION ('--icon-exp')
- Code was refactored to support other types of packaging
## [0.3.0] - 2019-07-02 ## [0.3.0] - 2019-07-02
### Features ### Features

View File

@@ -45,7 +45,8 @@ You can change some application settings via environment variables or arguments
- **FPAKMAN_UPDATE_NOTIFICATION**: enable or disable system updates notifications. Use **0** (disable) or **1** (enable, default). - **FPAKMAN_UPDATE_NOTIFICATION**: enable or disable system updates notifications. Use **0** (disable) or **1** (enable, default).
- **FPAKMAN_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60. - **FPAKMAN_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60.
- **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale. - **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale.
- **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for application data retrieved from the origin API. Default: 3600 (1 hour). - **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for cached API data. Default: 3600 (1 hour).
- **FPAKMAN_ICON_EXPIRATION**: define a custom expiration time in SECONDS for cached icons. Default: 300 (5 minutes).
### Roadmap ### Roadmap

View File

@@ -8,8 +8,10 @@ from colorama import Fore
from fpakman import __version__ from fpakman import __version__
from fpakman.core import resource from fpakman.core import resource
from fpakman.core import util from fpakman.util import util
from fpakman.core.controller import FlatpakManager from fpakman.core.controller import FlatpakManager
from fpakman.util.cache import Cache
from fpakman.util.memory import CacheCleaner
from fpakman.view.qt.systray import TrayIcon from fpakman.view.qt.systray import TrayIcon
app_name = 'fpakman' app_name = 'fpakman'
@@ -25,15 +27,19 @@ def log_msg(msg: str, color: int = None):
parser = argparse.ArgumentParser(prog=app_name, description="GUI for Flatpak applications management") parser = argparse.ArgumentParser(prog=app_name, description="GUI for Flatpak applications management")
parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__))
parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached application expiration time in SECONDS. Default: %(default)s') parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached API data expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('FPAKMAN_ICON_EXPIRATION', 60 * 5)), type=int, help='cached icons expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s') parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s')
parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s') parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s')
parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enable/disable system notifications for new updates. Default: %(default)s') parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enable/disable system notifications for new updates. Default: %(default)s')
args = parser.parse_args() args = parser.parse_args()
if args.cache_exp <= 0: if args.cache_exp < 0:
log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW) log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if args.icon_exp < 0:
log_msg("'icon-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if not args.locale.strip(): if not args.locale.strip():
log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED) log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED)
exit(1) exit(1)
@@ -45,18 +51,27 @@ if args.check_interval <= 0:
if args.update_notification == 0: if args.update_notification == 0:
log_msg('updates notifications are disabled', Fore.YELLOW) log_msg('updates notifications are disabled', Fore.YELLOW)
caches = []
flatpak_api_cache = Cache(expiration_time=args.cache_exp)
caches.append(flatpak_api_cache)
icon_cache = Cache(expiration_time=args.icon_exp)
caches.append(icon_cache)
locale_keys = util.get_locale_keys(args.locale) locale_keys = util.get_locale_keys(args.locale)
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg'))) app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
manager = FlatpakManager(cache_expire=args.cache_exp) manager = FlatpakManager(flatpak_api_cache)
trayIcon = TrayIcon(locale_keys=locale_keys, trayIcon = TrayIcon(locale_keys=locale_keys,
manager=manager, manager=manager,
check_interval=args.check_interval, check_interval=args.check_interval,
icon_cache=icon_cache,
update_notification=bool(args.update_notification)) update_notification=bool(args.update_notification))
trayIcon.load_database()
trayIcon.show() trayIcon.show()
CacheCleaner(caches).start()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@@ -0,0 +1,2 @@
FLATHUB_URL = 'https://flathub.org'
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'

View File

@@ -1,80 +1,53 @@
from datetime import datetime, timedelta from datetime import datetime
from threading import Lock from threading import Lock
from typing import List from typing import List
import requests import requests
from fpakman.core import flatpak from fpakman.core import flatpak
from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus
__FLATHUB_URL__ = 'https://flathub.org' from fpakman.core.worker import FlatpakAsyncDataLoaderManager
__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' from fpakman.util.cache import Cache
class FlatpakManager: class FlatpakManager:
def __init__(self, cache_expire: int = 60 * 60): def __init__(self, api_cache: Cache):
self.cache_apps = {} self.api_cache = api_cache
self.cache_expire = cache_expire
self.http_session = requests.Session() self.http_session = requests.Session()
self.lock_db_read = Lock()
self.lock_read = Lock() self.lock_read = Lock()
self.async_data_loader = FlatpakAsyncDataLoaderManager(api_cache=self.api_cache)
def load_full_database(self): def _map_to_model(self, app: dict) -> FlatpakApplication:
self.lock_db_read.acquire() model = FlatpakApplication(arch=app.get('arch'),
branch=app.get('branch'),
origin=app.get('origin'),
runtime=app.get('runtime'),
ref=app.get('ref'),
commit=app.get('commit'),
base_data=ApplicationData(id=app.get('id'),
name=app.get('name'),
version=app.get('version'),
latest_version=app.get('latest_version')))
try: api_data = self.api_cache.get(app['id'])
res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30)
if res.status_code == 200: expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
for app in res.json():
self.cache_apps[app['flatpakAppId']] = app
finally:
self.lock_db_read.release()
def _request_app_data(self, app_id: str): if not api_data or expired_data:
if not app['runtime']:
model.status = ApplicationStatus.LOADING_DATA
self.async_data_loader.load(model)
try: else: # filling cached data
res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30) for attr, val in api_data.items():
if attr != 'expires_at' and val:
setattr(model.base_data, attr, val)
if res.status_code == 200: return model
return res.json()
else:
print("Could not retrieve app data for id '{}'. Server response: {}".format(app_id, res.status_code))
except:
print("Could not retrieve app data for id '{}'. Timeout".format(app_id))
return None
def _fill_api_data(self, app: dict): def search(self, word: str) -> List[FlatpakApplication]:
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:
if self.cache_expire > 0:
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 = [] res = []
apps_found = flatpak.search(word) apps_found = flatpak.search(word)
@@ -82,25 +55,24 @@ class FlatpakManager:
if apps_found: if apps_found:
already_read = set() already_read = set()
installed_apps = self.read_installed() installed_apps = self.read_installed(keep_workers=True)
if installed_apps: if installed_apps:
for app in apps_found: for app_found in apps_found:
for installed_app in installed_apps: for installed_app in installed_apps:
if app['id'] == installed_app['id']: if app_found['id'] == installed_app.base_data.id:
res.append(installed_app) res.append(installed_app)
already_read.add(app['id']) already_read.add(app_found['id'])
for app in apps_found: for app_found in apps_found:
if app['id'] not in already_read: if app_found['id'] not in already_read:
app['update'] = False res.append(self._map_to_model(app_found))
app['installed'] = False
self._fill_api_data(app) self.async_data_loader.stop_current_workers()
res.append(app)
return res return res
def read_installed(self) -> List[dict]: def read_installed(self, keep_workers: bool = False) -> List[FlatpakApplication]:
self.lock_read.acquire() self.lock_read.acquire()
@@ -112,23 +84,34 @@ class FlatpakManager:
available_updates = flatpak.list_updates_as_str() available_updates = flatpak.list_updates_as_str()
for app in installed: models = []
self._fill_api_data(app)
app['update'] = app['id'] in available_updates
app['installed'] = True
return installed for app in installed:
model = self._map_to_model(app)
model.installed = True
model.update = app['id'] in available_updates
models.append(model)
if not keep_workers:
self.async_data_loader.stop_current_workers()
return models
return []
finally: finally:
self.lock_read.release() self.lock_read.release()
def downgrade_app(self, app: dict, root_password: str): def downgrade_app(self, app: FlatpakApplication, root_password: str):
commits = flatpak.get_app_commits(app['ref'], app['origin']) commits = flatpak.get_app_commits(app.ref, app.origin)
commit_idx = commits.index(app['commit']) commit_idx = commits.index(app.commit)
# downgrade is not possible if the app current commit in the first one: # downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1: if commit_idx == len(commits) - 1:
return None return None
return flatpak.downgrade_and_stream(app['ref'], commits[commit_idx + 1], root_password) return flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password)
def clean_cache_for(self, app_id: str):
self.api_cache.delete(app_id)

View File

@@ -3,6 +3,7 @@ import subprocess
from typing import List from typing import List
from fpakman.core import system from fpakman.core import system
from fpakman.core.model import Application
BASE_CMD = 'flatpak' BASE_CMD = 'flatpak'

42
fpakman/core/model.py Normal file
View File

@@ -0,0 +1,42 @@
from abc import ABC
from enum import Enum
class ApplicationStatus(Enum):
READY = 1
LOADING_DATA = 2
class ApplicationData:
def __init__(self, id: str, version: str, name: str = None, description: str = None, latest_version: str = None, icon_url: str = None):
self.id = id
self.name = name
self.version = version
self.description = description
self.latest_version = latest_version
self.icon_url = icon_url
class Application(ABC):
def __init__(self, base_data: ApplicationData, status: ApplicationStatus = ApplicationStatus.READY, installed: bool = False, update: bool = False):
self.base_data = base_data
self.status = status
self.installed = installed
self.update = update
class FlatpakApplication(Application):
def __init__(self, base_data: ApplicationData, branch: str, arch: str, origin: str, runtime: bool, ref: str, commit: str):
super(FlatpakApplication, self).__init__(base_data=base_data)
self.ref = ref
self.branch = branch
self.arch = arch
self.origin = origin
self.runtime = runtime
self.commit = commit
def is_incomplete(self):
return self.base_data.description is None and self.base_data.icon_url

119
fpakman/core/worker.py Normal file
View File

@@ -0,0 +1,119 @@
import traceback
from io import StringIO
from threading import Thread
from typing import List
import requests
from colorama import Fore
from fpakman.core.constants import FLATHUB_API_URL, FLATHUB_URL
from fpakman.core.model import FlatpakApplication, ApplicationStatus
from fpakman.util.cache import Cache
class FlatpakAsyncDataLoader(Thread):
def __init__(self, http_session, api_cache: Cache, attempts: int = 3):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.apps = []
self.http_session = http_session
self.attempts = attempts
self.api_cache = api_cache
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.stop = False
def log_msg(self, msg: str, color: int = None):
final_msg = StringIO()
if color:
final_msg.write(str(color))
final_msg.write('[{}] '.format(self.id_))
final_msg.write(msg)
if color:
final_msg.write(Fore.RESET)
final_msg.seek(0)
print(final_msg.read())
def run(self):
while True:
if not self.apps and self.stop:
break # stop working
else:
if self.apps:
app = self.apps[0]
app.status = ApplicationStatus.LOADING_DATA
for _ in range(0, self.attempts):
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, app.base_data.id), timeout=30)
if res.status_code == 200 and res.text:
data = res.json()
if not app.base_data.version:
app.base_data.version = data.get('version')
app.base_data.description = data.get('description', data.get('summary', None))
app.base_data.icon_url = data.get('iconMobileUrl', None)
app.base_data.latest_version = data.get('currentReleaseVersion', app.base_data.version)
if app.base_data.icon_url and app.base_data.icon_url.startswith('/'):
app.base_data.icon_url = FLATHUB_URL + app.base_data.icon_url
self.api_cache.add(app.base_data.id, {
'description': app.base_data.description,
'icon_url': app.base_data.icon_url,
'latest_version': app.base_data.latest_version,
'version': app.base_data.version
})
app.status = ApplicationStatus.READY
break
else:
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(app.base_data.id, res.status_code, res.content.decode()), Fore.RED)
except:
self.log_msg("Could not retrieve app data for id '{}'".format(app.base_data.id), Fore.YELLOW)
traceback.print_exc()
del self.apps[0]
def add(self, app: FlatpakApplication):
self.apps.append(app)
def current_load(self):
return len(self.apps)
class FlatpakAsyncDataLoaderManager:
def __init__(self, api_cache: Cache, worker_load: int = 3, workers: List[FlatpakAsyncDataLoader] = []):
self.worker_load = worker_load
self.current_workers = workers
self.http_session = requests.Session()
self.api_cache = api_cache
def load(self, app: FlatpakApplication):
available_workers = [w for w in self.current_workers if w.current_load() < self.worker_load]
if available_workers:
worker = available_workers[0]
else: # new worker
worker = FlatpakAsyncDataLoader(http_session=self.http_session,
api_cache=self.api_cache)
worker.start()
self.current_workers.append(worker)
worker.add(app)
def stop_current_workers(self):
for w in self.current_workers:
w.stop = True
self.current_workers = []

0
fpakman/util/__init__.py Normal file
View File

50
fpakman/util/cache.py Normal file
View File

@@ -0,0 +1,50 @@
from datetime import datetime, timedelta
from threading import Lock
class Cache:
def __init__(self, expiration_time: int):
self.expiration_time = expiration_time
self._cache = {}
self.lock = Lock()
def is_enabled(self):
return self.expiration_time < 0 or self.expiration_time > 0
def add(self, key: str, val: object):
if self.is_enabled():
self.lock.acquire()
self._cache[key] = {'val': val, 'expires_at': datetime.utcnow() + timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
self.lock.release()
def get(self, key: str):
if self.is_enabled():
val = self._cache.get(key)
if val:
expiration = val.get('expires_at')
if expiration and expiration <= datetime.utcnow():
self.lock.acquire()
del self._cache[key]
self.lock.release()
return None
return val['val']
def delete(self, key):
if self.is_enabled():
if key in self._cache:
self.lock.acquire()
del self._cache[key]
self.lock.release()
def keys(self):
return set(self._cache.keys()) if self.is_enabled() else set()
def clean_expired(self):
if self.is_enabled():
for key in self.keys():
self.get(key)

25
fpakman/util/memory.py Normal file
View File

@@ -0,0 +1,25 @@
import time
from threading import Thread
from typing import List
from fpakman.util.cache import Cache
import gc
class CacheCleaner(Thread):
def __init__(self, caches: List[Cache], check_interval: int = 15):
super(CacheCleaner, self).__init__(daemon=True)
self.caches = [c for c in caches if c.is_enabled()]
self.check_interval = check_interval
def run(self):
if self.caches:
while True:
for cache in self.caches:
cache.clean_expired()
gc.collect()
time.sleep(self.check_interval)

View File

@@ -1,20 +1,24 @@
from threading import Lock
from typing import List from typing import List
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel QHeaderView, QLabel
from fpakman.core import resource, util from fpakman.core import resource
from fpakman.core.model import FlatpakApplication, ApplicationStatus
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView, ApplicationViewStatus
class UpdateToggleButton(QToolButton): class UpdateToggleButton(QToolButton):
def __init__(self, model: dict, root: QWidget, locale_keys: dict, checked: bool = True): def __init__(self, app_view: ApplicationView, root: QWidget, locale_keys: dict, checked: bool = True):
super(UpdateToggleButton, self).__init__() super(UpdateToggleButton, self).__init__()
self.app = model self.app_view = app_view
self.root = root self.root = root
self.setCheckable(True) self.setCheckable(True)
self.clicked.connect(self.change_state) self.clicked.connect(self.change_state)
@@ -28,14 +32,14 @@ class UpdateToggleButton(QToolButton):
self.click() self.click()
def change_state(self, not_checked: bool): def change_state(self, not_checked: bool):
self.app['update_checked'] = not not_checked self.app_view.updated_checked = not not_checked
self.setIcon(self.icon_on if not not_checked else self.icon_off) self.setIcon(self.icon_on if not not_checked else self.icon_off)
self.root.change_update_state() self.root.change_update_state()
class AppsTable(QTableWidget): class AppsTable(QTableWidget):
def __init__(self, parent: QWidget): def __init__(self, parent: QWidget, icon_cache: Cache):
super(AppsTable, self).__init__() super(AppsTable, self).__init__()
self.setParent(parent) self.setParent(parent)
self.window = parent self.window = parent
@@ -44,7 +48,7 @@ class AppsTable(QTableWidget):
'manage_window.columns.latest_version', 'manage_window.columns.latest_version',
'flatpak.info.branch', 'flatpak.info.branch',
'flatpak.info.arch', 'flatpak.info.arch',
'flatpak.info.id', 'flatpak.info.description',
'flatpak.info.origin', 'flatpak.info.origin',
'manage_window.columns.installed', 'manage_window.columns.installed',
'manage_window.columns.update']] 'manage_window.columns.update']]
@@ -60,7 +64,8 @@ class AppsTable(QTableWidget):
self.network_man = QNetworkAccessManager() self.network_man = QNetworkAccessManager()
self.network_man.finished.connect(self._load_icon) self.network_man.finished.connect(self._load_icon)
self.icon_cache = {} self.icon_cache = icon_cache
self.lock_async_data = Lock()
def contextMenuEvent(self, QContextMenuEvent): # selected row right click event def contextMenuEvent(self, QContextMenuEvent): # selected row right click event
@@ -68,7 +73,7 @@ class AppsTable(QTableWidget):
menu_row = QMenu() menu_row = QMenu()
if app['model']['installed']: if app.model.installed:
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"]) 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.setIcon(QIcon(resource.get_path('img/info.svg')))
action_info.triggered.connect(self._get_app_info) action_info.triggered.connect(self._get_app_info)
@@ -84,7 +89,7 @@ class AppsTable(QTableWidget):
action_uninstall.triggered.connect(self._uninstall_app) action_uninstall.triggered.connect(self._uninstall_app)
menu_row.addAction(action_uninstall) menu_row.addAction(action_uninstall)
if not app['model']['runtime']: # not available for runtimes if isinstance(app.model, FlatpakApplication) and not app.model.runtime: # not available for runtimes
action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"]) action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"])
action_downgrade.triggered.connect(self._downgrade_app) action_downgrade.triggered.connect(self._downgrade_app)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
@@ -99,25 +104,54 @@ class AppsTable(QTableWidget):
menu_row.popup(QCursor.pos()) menu_row.popup(QCursor.pos())
menu_row.exec_() menu_row.exec_()
def get_selected_app(self): def fill_async_data(self):
self.lock_async_data.acquire()
if self.window.apps:
for idx, app_v in enumerate(self.window.apps):
if app_v.visible and app_v.status == ApplicationViewStatus.LOADING and app_v.model.status == ApplicationStatus.READY:
self.network_man.get(QNetworkRequest(QUrl(app_v.model.base_data.icon_url)))
self.item(idx, 2).setText(app_v.model.base_data.latest_version)
desc = app_v.get_async_attr('description', strip_html=True)
self.item(idx, 5).setText(desc)
self.item(idx, 5).setToolTip(desc)
app_v.status = ApplicationViewStatus.READY
visible, ready = 0, 0
for app_v in self.window.apps:
if app_v.visible:
visible += 1
if app_v.status == ApplicationViewStatus.READY:
ready += 1
if ready == visible:
self.window.resize_and_center()
self.lock_async_data.release()
def get_selected_app(self) -> ApplicationView:
return self.window.apps[self.currentRow()] return self.window.apps[self.currentRow()]
def get_selected_app_icon(self): def get_selected_app_icon(self) -> QIcon:
return self.item(self.currentRow(), 0).icon() return self.item(self.currentRow(), 0).icon()
def _uninstall_app(self): def _uninstall_app(self):
selected_app = self.get_selected_app() selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys): locale_keys=self.window.locale_keys):
self.window.uninstall_app(selected_app['model']['ref']) self.window.uninstall_app(selected_app)
def _downgrade_app(self): def _downgrade_app(self):
selected_app = self.get_selected_app() selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'], if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']), body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys): locale_keys=self.window.locale_keys):
self.window.downgrade_app(selected_app) self.window.downgrade_app(selected_app)
@@ -132,94 +166,92 @@ class AppsTable(QTableWidget):
def _load_icon(self, http_response): def _load_icon(self, http_response):
icon_url = http_response.url().toString() icon_url = http_response.url().toString()
pixmap = QPixmap()
pixmap.loadFromData(http_response.readAll())
icon = QIcon(pixmap)
self.icon_cache[icon_url] = icon
for idx, app in enumerate(self.window.apps): if not self.icon_cache.get(icon_url):
if app['model']['icon'] == icon_url: pixmap = QPixmap()
self.item(idx, 0).setIcon(icon) pixmap.loadFromData(http_response.readAll())
self.window.resize_and_center() icon = QIcon(pixmap)
break self.icon_cache.add(icon_url, icon)
def update_apps(self, apps: List[dict]): for idx, app in enumerate(self.window.apps):
if app.model.base_data.icon_url == icon_url:
self.item(idx, 0).setIcon(icon)
break
def update_apps(self, app_views: List[ApplicationView]):
self.setEnabled(True) self.setEnabled(True)
self.setRowCount(len(apps) if apps else 0) self.setRowCount(len(app_views) if app_views else 0)
if apps: if app_views:
for idx, app in enumerate(apps): for idx, app_v in enumerate(app_views):
tooltip = util.strip_html(app['model']['description']) if app['model']['description'] else None
col_name = QTableWidgetItem() col_name = QTableWidgetItem()
col_name.setText(app['model']['name']) col_name.setText(app_v.model.base_data.name)
col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_name.setToolTip(tooltip)
if not app['model']['icon']: if not app_v.model.base_data.icon_url:
col_name.setIcon(self.icon_flathub) col_name.setIcon(self.icon_flathub)
else: else:
cached_icon = self.icon_cache.get(app['model']['icon']) cached_icon = self.icon_cache.get(app_v.model.base_data.icon_url)
if cached_icon: if cached_icon:
col_name.setIcon(cached_icon) col_name.setIcon(cached_icon)
else: else:
col_name.setIcon(self.icon_flathub) col_name.setIcon(self.icon_flathub)
self.network_man.get(QNetworkRequest(QUrl(app['model']['icon'])))
self.setItem(idx, 0, col_name) self.setItem(idx, 0, col_name)
col_version = QTableWidgetItem() col_version = QTableWidgetItem()
col_version.setText(app['model']['version']) col_version.setText(app_v.model.base_data.version)
col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_version.setToolTip(tooltip)
self.setItem(idx, 1, col_version) self.setItem(idx, 1, col_version)
col_release = QTableWidgetItem() col_release = QTableWidgetItem()
col_release.setText(app['model']['latest_version']) col_release.setText(app_v.get_async_attr('latest_version'))
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_release.setToolTip(tooltip)
self.setItem(idx, 2, col_release) self.setItem(idx, 2, col_release)
if app['model']['version'] and app['model']['latest_version'] and app['model']['version'] < app['model']['latest_version']: if app_v.model.base_data.version and app_v.model.base_data.latest_version and app_v.model.base_data.version < app_v.model.base_data.latest_version:
col_release.setForeground(QColor('orange')) col_release.setForeground(QColor('orange'))
col_branch = QTableWidgetItem() col_branch = QTableWidgetItem()
col_branch.setText(app['model']['branch']) col_branch.setText(app_v.model.branch if isinstance(app_v.model, FlatpakApplication) else '')
col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_branch.setToolTip(tooltip)
self.setItem(idx, 3, col_branch) self.setItem(idx, 3, col_branch)
col_arch = QTableWidgetItem() col_arch = QTableWidgetItem()
col_arch.setText(app['model']['arch']) col_arch.setText(app_v.model.arch if isinstance(app_v.model, FlatpakApplication) else '')
col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_arch.setToolTip(tooltip)
self.setItem(idx, 4, col_arch) self.setItem(idx, 4, col_arch)
col_id = QTableWidgetItem() desc = app_v.get_async_attr('description', strip_html=True)
col_id.setText(app['model']['id']) col_description = QTableWidgetItem()
col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_description.setText(desc)
col_id.setToolTip(tooltip) col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.setItem(idx, 5, col_id)
if app_v.model.status == ApplicationStatus.READY:
col_description.setToolTip(desc)
self.setItem(idx, 5, col_description)
col_origin = QTableWidgetItem() col_origin = QTableWidgetItem()
col_origin.setText(app['model']['origin']) col_origin.setText(app_v.model.origin if isinstance(app_v.model, FlatpakApplication) else '')
col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_origin.setToolTip(tooltip)
self.setItem(idx, 6, col_origin) self.setItem(idx, 6, col_origin)
col_installed = QLabel() col_installed = QLabel()
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross'))))) col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app_v.model.installed else 'red_cross')))))
col_installed.setToolTip(tooltip)
col_installed.setAlignment(Qt.AlignCenter) col_installed.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 7, col_installed) self.setCellWidget(idx, 7, col_installed)
col_update = UpdateToggleButton(app, self.window, self.window.locale_keys, app['model']['update']) if app['model']['update'] else None col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update) if app_v.model.update else None
self.setCellWidget(idx, 8, col_update) self.setCellWidget(idx, 8, col_update)
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
header_horizontal = self.horizontalHeader() header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()): for i in range(self.columnCount()):
header_horizontal.setSectionResizeMode(i, policy) if i == 5:
header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
else:
header_horizontal.setSectionResizeMode(i, policy)

View File

@@ -11,7 +11,7 @@ class HistoryDialog(QDialog):
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict): def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
super(HistoryDialog, self).__init__() super(HistoryDialog, self).__init__()
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name'])) self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name))
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
layout = QVBoxLayout() layout = QVBoxLayout()
@@ -29,7 +29,7 @@ class HistoryDialog(QDialog):
for row, commit in enumerate(app['commits']): for row, commit in enumerate(app['commits']):
current_app_commit = app['model']['commit'] == commit['commit'] current_app_commit = app['model'].commit == commit['commit']
for col, key in enumerate(sorted(commit.keys())): for col, key in enumerate(sorted(commit.keys())):
item = QTableWidgetItem() item = QTableWidgetItem()

View File

@@ -1,4 +1,5 @@
import time import time
from threading import Lock
from typing import List from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
@@ -7,6 +8,8 @@ from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from fpakman.core import resource, system from fpakman.core import resource, system
from fpakman.core.controller import FlatpakManager from fpakman.core.controller import FlatpakManager
from fpakman.core.model import Application
from fpakman.util.cache import Cache
from fpakman.view.qt.about import AboutDialog from fpakman.view.qt.about import AboutDialog
from fpakman.view.qt.window import ManageWindow from fpakman.view.qt.window import ManageWindow
@@ -26,7 +29,7 @@ class UpdateCheck(QThread):
apps = self.manager.read_installed() apps = self.manager.read_installed()
updates = [app for app in apps if app['update']] updates = [app for app in apps if app.update]
if updates: if updates:
self.signal.emit(updates) self.signal.emit(updates)
@@ -34,25 +37,13 @@ class UpdateCheck(QThread):
time.sleep(self.check_interval) 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): class TrayIcon(QSystemTrayIcon):
def __init__(self, locale_keys: dict, manager: FlatpakManager, check_interval: int = 60, update_notification: bool = True): def __init__(self, locale_keys: dict, manager: FlatpakManager, icon_cache: Cache, check_interval: int = 60, update_notification: bool = True):
super(TrayIcon, self).__init__() super(TrayIcon, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager
self.icon_cache = icon_cache
self.icon_default = QIcon(resource.get_path('img/logo.png')) self.icon_default = QIcon(resource.get_path('img/logo.png'))
self.icon_update = QIcon(resource.get_path('img/logo_update.png')) self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
@@ -60,12 +51,8 @@ class TrayIcon(QSystemTrayIcon):
self.menu = QMenu() 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 = self.menu.addAction(self.locale_keys['tray.action.manage'])
self.action_manage.triggered.connect(self.show_manage_window) 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 = self.menu.addAction(self.locale_keys['tray.action.about'])
self.action_about.triggered.connect(self.show_about) self.action_about.triggered.connect(self.show_about)
@@ -82,46 +69,45 @@ class TrayIcon(QSystemTrayIcon):
self.dialog_about = None self.dialog_about = None
self.thread_database = LoadDatabase(manager)
self.thread_database.signal_finished.connect(self._update_menu)
self.last_updates = set() self.last_updates = set()
self.update_notification = update_notification self.update_notification = update_notification
self.lock_notify = Lock()
def load_database(self): def notify_updates(self, updates: List[Application]):
self.thread_database.start()
def _update_menu(self): self.lock_notify.acquire()
self.action_refreshing.setVisible(False)
self.action_manage.setVisible(True)
def notify_updates(self, updates: List[dict]): try:
if len(updates) > 0:
if len(updates) > 0: update_keys = {'{}:{}'.format(app.base_data.id, app.base_data.version) for app in updates}
update_keys = {'{}:{}'.format(app['id'], app['latest_version']) for app in updates} new_icon = self.icon_update
new_icon = self.icon_update if update_keys.difference(self.last_updates):
self.last_updates = update_keys
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'].format('Flatpak'), len(updates))
self.setToolTip(msg)
if update_keys.difference(self.last_updates): if self.update_notification:
self.last_updates = update_keys system.notify_user(msg)
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'].format('Flatpak'), len(updates))
self.setToolTip(msg)
if self.update_notification: else:
system.notify_user(msg) new_icon = self.icon_default
self.setToolTip(None)
else: if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
new_icon = self.icon_default self.setIcon(new_icon)
self.setToolTip(None)
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed finally:
self.setIcon(new_icon) self.lock_notify.release()
def show_manage_window(self): def show_manage_window(self):
if self.manage_window is None: if self.manage_window is None:
self.manage_window = ManageWindow(locale_keys=self.locale_keys, self.manage_window = ManageWindow(locale_keys=self.locale_keys,
manager=self.manager, manager=self.manager,
icon_cache=self.icon_cache,
tray_icon=self) tray_icon=self)
if self.manage_window.isMinimized(): if self.manage_window.isMinimized():

View File

@@ -1,10 +1,15 @@
import time import time
from datetime import datetime, timedelta
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from fpakman.core import flatpak from fpakman.core import flatpak
from fpakman.core.controller import FlatpakManager from fpakman.core.controller import FlatpakManager
from fpakman.core.model import ApplicationStatus, FlatpakApplication
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView, ApplicationViewStatus
class UpdateSelectedApps(QThread): class UpdateSelectedApps(QThread):
@@ -43,17 +48,21 @@ class UninstallApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal()
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self): def __init__(self, manager: FlatpakManager, icon_cache: Cache, app: ApplicationView = None):
super(UninstallApp, self).__init__() super(UninstallApp, self).__init__()
self.app_ref = None self.app = app
self.manager = manager
self.icon_cache = icon_cache
def run(self): def run(self):
if self.app_ref: if self.app and isinstance(self.app.model, FlatpakApplication):
for output in flatpak.uninstall_and_stream(self.app_ref): for output in flatpak.uninstall_and_stream(self.app.model.ref):
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
self.icon_cache.delete(self.app.model.base_data.icon_url)
self.manager.clean_cache_for(self.app.model.base_data.id)
self.signal_finished.emit() self.signal_finished.emit()
@@ -61,23 +70,23 @@ class DowngradeApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal()
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self, manager: FlatpakManager, locale_keys: dict): def __init__(self, manager: FlatpakManager, locale_keys: dict, app: ApplicationView = None):
super(DowngradeApp, self).__init__() super(DowngradeApp, self).__init__()
self.manager = manager self.manager = manager
self.app = None self.app = app
self.root_password = None
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.root_password = None
def run(self): def run(self):
if self.app: if self.app and isinstance(self.app.model, FlatpakApplication):
stream = self.manager.downgrade_app(self.app['model'], self.root_password) stream = self.manager.downgrade_app(self.app.model, self.root_password)
if stream is None: if stream is None:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body']) body=self.locale_keys['popup.downgrade.impossible.body'])
else: else:
for output in self.manager.downgrade_app(self.app['model'], self.root_password): for output in self.manager.downgrade_app(self.app.model, self.root_password):
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
@@ -90,16 +99,16 @@ class DowngradeApp(QThread):
class GetAppInfo(QThread): class GetAppInfo(QThread):
signal_finished = pyqtSignal(dict) signal_finished = pyqtSignal(dict)
def __init__(self): def __init__(self, app: ApplicationView = None):
super(GetAppInfo, self).__init__() super(GetAppInfo, self).__init__()
self.app = None self.app = app
def run(self): def run(self):
if self.app: if self.app and isinstance(self.app.model, FlatpakApplication):
app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch']) app_info = flatpak.get_app_info_fields(self.app.model.base_data.id, self.app.model.branch)
app_info['name'] = self.app['model']['name'] app_info['name'] = self.app.model.base_data.name
app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app' app_info['type'] = 'runtime' if self.app.model.runtime else 'app'
app_info['description'] = self.app['model']['description'] app_info['description'] = self.app.model.base_data.description
self.signal_finished.emit(app_info) self.signal_finished.emit(app_info)
self.app = None self.app = None
@@ -107,14 +116,14 @@ class GetAppInfo(QThread):
class GetAppHistory(QThread): class GetAppHistory(QThread):
signal_finished = pyqtSignal(dict) signal_finished = pyqtSignal(dict)
def __init__(self): def __init__(self, app: ApplicationView = None):
super(GetAppHistory, self).__init__() super(GetAppHistory, self).__init__()
self.app = None self.app = app
def run(self): def run(self):
if self.app: if self.app and isinstance(self.app.model, FlatpakApplication):
commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin']) 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.signal_finished.emit({'model': self.app.model, 'commits': commits})
self.app = None self.app = None
@@ -141,14 +150,14 @@ class InstallApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal()
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self): def __init__(self, app: ApplicationView = None):
super(InstallApp, self).__init__() super(InstallApp, self).__init__()
self.app = None self.app = app
def run(self): def run(self):
if self.app: if self.app and isinstance(self.app.model, FlatpakApplication):
for output in flatpak.install_and_stream(self.app['model']['id'], self.app['model']['origin']): for output in flatpak.install_and_stream(self.app.model.base_data.id, self.app.model.origin):
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
@@ -184,3 +193,36 @@ class AnimateProgress(QThread):
time.sleep(0.05) time.sleep(0.05)
self.progress_value = 0 self.progress_value = 0
class VerifyModels(QThread):
signal_updates = pyqtSignal()
def __init__(self, apps: List[ApplicationView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
def run(self):
if self.apps:
stop_at = datetime.utcnow() + timedelta(seconds=30)
last_ready = 0
while True:
current_ready = 0
for app in self.apps:
current_ready += 1 if app.model.status == ApplicationStatus.READY else 0
if current_ready > last_ready:
last_ready = current_ready
self.signal_updates.emit()
if current_ready == len(self.apps):
self.signal_updates.emit()
break
if stop_at <= datetime.utcnow():
break

View File

@@ -0,0 +1,27 @@
from enum import Enum
from fpakman.util import util
from fpakman.core.model import Application, ApplicationStatus, FlatpakApplication
class ApplicationViewStatus(Enum):
LOADING = 0
READY = 1
class ApplicationView:
def __init__(self, model: Application, visible: bool = True):
self.model = model
self.update_checked = model.update
self.visible = visible
self.status = ApplicationViewStatus.LOADING
def get_async_attr(self, attr: str, strip_html: bool = False, default: str = '...'):
if isinstance(self.model, FlatpakApplication) and self.model.runtime:
res = getattr(self.model.base_data, attr)
else:
res = getattr(self.model.base_data, attr) if self.model.status == ApplicationStatus.READY and getattr(self.model.base_data, attr) else default
return util.strip_html(res) if res and strip_html else res

View File

@@ -10,13 +10,16 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHead
from fpakman.core import resource, flatpak from fpakman.core import resource, flatpak
from fpakman.core.controller import FlatpakManager from fpakman.core.controller import FlatpakManager
from fpakman.core.model import Application, FlatpakApplication
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.apps_table import AppsTable from fpakman.view.qt.apps_table import AppsTable
from fpakman.view.qt.history import HistoryDialog from fpakman.view.qt.history import HistoryDialog
from fpakman.view.qt.info import InfoDialog from fpakman.view.qt.info import InfoDialog
from fpakman.view.qt.root import is_root, ask_root_password from fpakman.view.qt.root import is_root, ask_root_password
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels
from fpakman.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500' DARK_ORANGE = '#FF4500'
@@ -24,7 +27,7 @@ DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget): class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400 __BASE_HEIGHT__ = 400
def __init__(self, locale_keys: dict, manager: FlatpakManager, tray_icon=None): def __init__(self, locale_keys: dict, icon_cache: Cache, manager: FlatpakManager, tray_icon=None):
super(ManageWindow, self).__init__() super(ManageWindow, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager
@@ -33,6 +36,7 @@ class ManageWindow(QWidget):
self.working = False # restrict the number of threaded actions self.working = False # restrict the number of threaded actions
self.apps = [] self.apps = []
self.label_flatpak = None self.label_flatpak = None
self.icon_cache = icon_cache
self.icon_flathub = QIcon(resource.get_path('img/logo.svg')) self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
self._check_flatpak_installed() self._check_flatpak_installed()
@@ -101,7 +105,7 @@ class ManageWindow(QWidget):
self.layout.addWidget(toolbar) self.layout.addWidget(toolbar)
self.table_apps = AppsTable(self) self.table_apps = AppsTable(self, self.icon_cache)
self.table_apps.change_headers_policy() self.table_apps.change_headers_policy()
self.layout.addWidget(self.table_apps) self.layout.addWidget(self.table_apps)
@@ -131,7 +135,7 @@ class ManageWindow(QWidget):
self.thread_refresh = RefreshApps(self.manager) self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh) self.thread_refresh.signal.connect(self._finish_refresh)
self.thread_uninstall = UninstallApp() self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
self.thread_uninstall.signal_output.connect(self._update_action_output) self.thread_uninstall.signal_output.connect(self._update_action_output)
self.thread_uninstall.signal_finished.connect(self._finish_uninstall) self.thread_uninstall.signal_finished.connect(self._finish_uninstall)
@@ -155,6 +159,9 @@ class ManageWindow(QWidget):
self.thread_animate_progress = AnimateProgress() self.thread_animate_progress = AnimateProgress()
self.thread_animate_progress.signal_change.connect(self._update_progress) self.thread_animate_progress.signal_change.connect(self._update_progress)
self.thread_verify_models = VerifyModels()
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)
self.toolbar_bottom = QToolBar() self.toolbar_bottom = QToolBar()
self.label_updates = QLabel('') self.label_updates = QLabel('')
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
@@ -175,6 +182,9 @@ class ManageWindow(QWidget):
self.centralize() self.centralize()
def _notify_model_data_change(self):
self.table_apps.fill_async_data()
def _new_spacer(self): def _new_spacer(self):
spacer = QWidget() spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -250,20 +260,20 @@ class ManageWindow(QWidget):
self.thread_refresh.start() self.thread_refresh.start()
def _finish_refresh(self, apps: List[dict]): def _finish_refresh(self, apps: List[Application]):
self.update_apps(apps) self.update_apps(apps)
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
def uninstall_app(self, app_ref: str): def uninstall_app(self, app: ApplicationView):
self._check_flatpak_installed() self._check_flatpak_installed()
if self._acquire_lock(): if self._acquire_lock():
self._handle_console_option(True) self._handle_console_option(True)
self._begin_action(self.locale_keys['manage_window.status.uninstalling']) self._begin_action(self.locale_keys['manage_window.status.uninstalling'])
self.thread_uninstall.app_ref = app_ref self.thread_uninstall.app = app
self.thread_uninstall.start() self.thread_uninstall.start()
def _finish_uninstall(self): def _finish_uninstall(self):
@@ -281,10 +291,10 @@ class ManageWindow(QWidget):
if self.apps: if self.apps:
show_only_apps = True if only_apps == 2 else False show_only_apps = True if only_apps == 2 else False
for idx, app in enumerate(self.apps): for idx, app_v in enumerate(self.apps):
hidden = show_only_apps and app['model']['runtime'] hidden = show_only_apps and isinstance(app_v.model, FlatpakApplication) and app_v.model.runtime
self.table_apps.setRowHidden(idx, hidden) self.table_apps.setRowHidden(idx, hidden)
app['visible'] = not hidden app_v.visible = not hidden
self.change_update_state() self.change_update_state()
self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy(QHeaderView.Stretch)
@@ -297,9 +307,9 @@ class ManageWindow(QWidget):
app_updates, runtime_updates = 0, 0 app_updates, runtime_updates = 0, 0
for app in self.apps: for app_v in self.apps:
if app['model']['update']: if app_v.model.update:
if app['model']['runtime']: if app_v.model.runtime:
runtime_updates += 1 runtime_updates += 1
else: else:
app_updates += 1 app_updates += 1
@@ -314,13 +324,13 @@ class ManageWindow(QWidget):
else: else:
self.label_updates.setText('') self.label_updates.setText('')
for app in self.apps: for app_v in self.apps:
if app['visible'] and app['update_checked']: if app_v.visible and app_v.update_checked:
enable_bt_update = True enable_bt_update = True
break break
self.bt_upgrade.setEnabled(enable_bt_update) self.bt_upgrade.setEnabled(enable_bt_update)
self.tray_icon.notify_updates([app['model'] for app in self.apps if app['model']['update']]) self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
def centralize(self): def centralize(self):
geo = self.frameGeometry() geo = self.frameGeometry()
@@ -329,7 +339,7 @@ class ManageWindow(QWidget):
geo.moveCenter(center_point) geo.moveCenter(center_point)
self.move(geo.topLeft()) self.move(geo.topLeft())
def update_apps(self, apps: List[dict]): def update_apps(self, apps: List[Application]):
self._check_flatpak_installed() self._check_flatpak_installed()
self.apps = [] self.apps = []
@@ -338,11 +348,10 @@ class ManageWindow(QWidget):
if apps: if apps:
for app in apps: for app in apps:
app_model = {'model': app, app_model = ApplicationView(model=app,
'update_checked': app['update'], visible=(isinstance(app, FlatpakApplication) and not app.runtime) or not self.checkbox_only_apps.isChecked())
'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()}
napps += 1 if not app['runtime'] else 0 napps += 1 if isinstance(app, FlatpakApplication) and not app.runtime else 0
self.apps.append(app_model) self.apps.append(app_model)
if napps == 0: if napps == 0:
@@ -357,9 +366,11 @@ class ManageWindow(QWidget):
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0) self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
self.resize_and_center() self.resize_and_center()
self.thread_verify_models.apps = self.apps
self.thread_verify_models.start()
def resize_and_center(self): def resize_and_center(self):
new_width = reduce(operator.add, new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05
[self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05
self.resize(new_width, self.height()) self.resize(new_width, self.height())
self.centralize() self.centralize()
@@ -368,7 +379,7 @@ class ManageWindow(QWidget):
if self._acquire_lock(): if self._acquire_lock():
if self.apps: if self.apps:
to_update = [pak['model']['ref'] for pak in self.apps if pak['visible'] and pak['update_checked']] to_update = [app_v.model.ref for app_v in self.apps if app_v.visible and app_v.update_checked and isinstance(app_v.model, FlatpakApplication)]
if to_update: if to_update:
self._handle_console_option(True) self._handle_console_option(True)
@@ -390,7 +401,7 @@ class ManageWindow(QWidget):
self.thread_animate_progress.stop = False self.thread_animate_progress.stop = False
self.thread_animate_progress.start() self.thread_animate_progress.start()
self.ref_progress_bar.setVisible(True) self.ref_progress_bar.setVisible(True)
self.progress_bar.setValue(50)
self.label_status.setText(action_label + "...") self.label_status.setText(action_label + "...")
self.toolbar_search.setVisible(False) self.toolbar_search.setVisible(False)
self.bt_upgrade.setEnabled(False) self.bt_upgrade.setEnabled(False)
@@ -398,12 +409,11 @@ class ManageWindow(QWidget):
self.checkbox_only_apps.setEnabled(False) self.checkbox_only_apps.setEnabled(False)
self.table_apps.setEnabled(False) self.table_apps.setEnabled(False)
def finish_action(self, clear_search: bool = True): def finish_action(self):
self.ref_progress_bar.setVisible(False)
self.ref_label_updates.setVisible(True) self.ref_label_updates.setVisible(True)
self.thread_animate_progress.stop = True self.thread_animate_progress.stop = True
self.ref_progress_bar.setVisible(False)
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
self.bt_refresh.setEnabled(True) self.bt_refresh.setEnabled(True)
self.toolbar_search.setVisible(True) self.toolbar_search.setVisible(True)
self.checkbox_only_apps.setEnabled(True) self.checkbox_only_apps.setEnabled(True)
@@ -411,9 +421,6 @@ class ManageWindow(QWidget):
self.input_search.setEnabled(True) self.input_search.setEnabled(True)
self.label_status.setText('') self.label_status.setText('')
if clear_search:
self.input_search.setText('')
def downgrade_app(self, app: dict): def downgrade_app(self, app: dict):
self._check_flatpak_installed() self._check_flatpak_installed()
@@ -477,10 +484,10 @@ class ManageWindow(QWidget):
self.thread_search.word = word self.thread_search.word = word
self.thread_search.start() self.thread_search.start()
def _finish_search(self, apps_found: List[dict]): def _finish_search(self, apps_found: List[Application]):
self._release_lock() self._release_lock()
self.finish_action(clear_search=False) self.finish_action()
self.update_apps(apps_found) self.update_apps(apps_found)
def install_app(self, app: dict): def install_app(self, app: dict):