From 74fa85a2bb1acb5a72c1ce5bde54d6d70b420cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?= Date: Mon, 8 Jul 2019 12:15:15 -0300 Subject: [PATCH] improving initialization time and reducing async overload --- CHANGELOG.md | 5 ++ README.md | 3 +- fpakman/app.py | 25 ++++-- fpakman/core/constants.py | 2 + fpakman/core/controller.py | 137 ++++++++++++++----------------- fpakman/core/flatpak.py | 1 + fpakman/core/model.py | 42 ++++++++++ fpakman/core/worker.py | 119 +++++++++++++++++++++++++++ fpakman/util/__init__.py | 0 fpakman/util/cache.py | 50 ++++++++++++ fpakman/util/memory.py | 25 ++++++ fpakman/{core => util}/util.py | 0 fpakman/view/qt/apps_table.py | 142 ++++++++++++++++++++------------- fpakman/view/qt/history.py | 4 +- fpakman/view/qt/systray.py | 68 +++++++--------- fpakman/view/qt/thread.py | 94 ++++++++++++++++------ fpakman/view/qt/view_model.py | 27 +++++++ fpakman/view/qt/window.py | 73 +++++++++-------- 18 files changed, 577 insertions(+), 240 deletions(-) create mode 100644 fpakman/core/constants.py create mode 100644 fpakman/core/model.py create mode 100644 fpakman/core/worker.py create mode 100644 fpakman/util/__init__.py create mode 100644 fpakman/util/cache.py create mode 100644 fpakman/util/memory.py rename fpakman/{core => util}/util.py (100%) create mode 100644 fpakman/view/qt/view_model.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 871544e7..b8f8bc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.3.1] ### Improvements - 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 ### Features diff --git a/README.md b/README.md index d6e35040..01efec3c 100644 --- a/README.md +++ b/README.md @@ -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_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_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 diff --git a/fpakman/app.py b/fpakman/app.py index f23df03b..9e6f5fe4 100755 --- a/fpakman/app.py +++ b/fpakman/app.py @@ -8,8 +8,10 @@ from colorama import Fore from fpakman import __version__ from fpakman.core import resource -from fpakman.core import util +from fpakman.util import util 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 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.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('-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') 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) +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(): log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED) exit(1) @@ -45,18 +51,27 @@ if args.check_interval <= 0: if args.update_notification == 0: 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) app = QApplication(sys.argv) 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, manager=manager, check_interval=args.check_interval, + icon_cache=icon_cache, update_notification=bool(args.update_notification)) -trayIcon.load_database() trayIcon.show() +CacheCleaner(caches).start() + sys.exit(app.exec_()) diff --git a/fpakman/core/constants.py b/fpakman/core/constants.py new file mode 100644 index 00000000..687356af --- /dev/null +++ b/fpakman/core/constants.py @@ -0,0 +1,2 @@ +FLATHUB_URL = 'https://flathub.org' +FLATHUB_API_URL = FLATHUB_URL + '/api/v1' diff --git a/fpakman/core/controller.py b/fpakman/core/controller.py index 5e02af2d..d0337496 100755 --- a/fpakman/core/controller.py +++ b/fpakman/core/controller.py @@ -1,80 +1,53 @@ -from datetime import datetime, timedelta +from datetime import datetime from threading import Lock from typing import List import requests from fpakman.core import flatpak - -__FLATHUB_URL__ = 'https://flathub.org' -__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' +from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus +from fpakman.core.worker import FlatpakAsyncDataLoaderManager +from fpakman.util.cache import Cache class FlatpakManager: - def __init__(self, cache_expire: int = 60 * 60): - self.cache_apps = {} - self.cache_expire = cache_expire + def __init__(self, api_cache: Cache): + self.api_cache = api_cache self.http_session = requests.Session() - self.lock_db_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: - res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30) + api_data = self.api_cache.get(app['id']) - if res.status_code == 200: - for app in res.json(): - self.cache_apps[app['flatpakAppId']] = app - finally: - self.lock_db_read.release() + expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow() - 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: - res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30) + else: # filling cached data + 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 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 + return model - 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: - - 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]: + def search(self, word: str) -> List[FlatpakApplication]: res = [] apps_found = flatpak.search(word) @@ -82,25 +55,24 @@ class FlatpakManager: if apps_found: already_read = set() - installed_apps = self.read_installed() + installed_apps = self.read_installed(keep_workers=True) if installed_apps: - for app in apps_found: + for app_found in apps_found: 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) - already_read.add(app['id']) + already_read.add(app_found['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) + for app_found in apps_found: + if app_found['id'] not in already_read: + res.append(self._map_to_model(app_found)) + + self.async_data_loader.stop_current_workers() return res - def read_installed(self) -> List[dict]: + def read_installed(self, keep_workers: bool = False) -> List[FlatpakApplication]: self.lock_read.acquire() @@ -112,23 +84,34 @@ class FlatpakManager: available_updates = flatpak.list_updates_as_str() - for app in installed: - self._fill_api_data(app) - app['update'] = app['id'] in available_updates - app['installed'] = True + models = [] - 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: 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']) - commit_idx = commits.index(app['commit']) + commits = flatpak.get_app_commits(app.ref, app.origin) + commit_idx = commits.index(app.commit) # downgrade is not possible if the app current commit in the first one: if commit_idx == len(commits) - 1: 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) diff --git a/fpakman/core/flatpak.py b/fpakman/core/flatpak.py index 5da0358b..7470c839 100755 --- a/fpakman/core/flatpak.py +++ b/fpakman/core/flatpak.py @@ -3,6 +3,7 @@ import subprocess from typing import List from fpakman.core import system +from fpakman.core.model import Application BASE_CMD = 'flatpak' diff --git a/fpakman/core/model.py b/fpakman/core/model.py new file mode 100644 index 00000000..1bc58ea9 --- /dev/null +++ b/fpakman/core/model.py @@ -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 diff --git a/fpakman/core/worker.py b/fpakman/core/worker.py new file mode 100644 index 00000000..170ecc2e --- /dev/null +++ b/fpakman/core/worker.py @@ -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 = [] diff --git a/fpakman/util/__init__.py b/fpakman/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fpakman/util/cache.py b/fpakman/util/cache.py new file mode 100644 index 00000000..4fc2a434 --- /dev/null +++ b/fpakman/util/cache.py @@ -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) diff --git a/fpakman/util/memory.py b/fpakman/util/memory.py new file mode 100644 index 00000000..35ec2659 --- /dev/null +++ b/fpakman/util/memory.py @@ -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) + diff --git a/fpakman/core/util.py b/fpakman/util/util.py similarity index 100% rename from fpakman/core/util.py rename to fpakman/util/util.py diff --git a/fpakman/view/qt/apps_table.py b/fpakman/view/qt/apps_table.py index e27a9f4a..a9393002 100644 --- a/fpakman/view/qt/apps_table.py +++ b/fpakman/view/qt/apps_table.py @@ -1,20 +1,24 @@ +from threading import Lock from typing import List from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor -from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ 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.view_model import ApplicationView, ApplicationViewStatus 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__() - self.app = model + self.app_view = app_view self.root = root self.setCheckable(True) self.clicked.connect(self.change_state) @@ -28,14 +32,14 @@ class UpdateToggleButton(QToolButton): self.click() 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.root.change_update_state() class AppsTable(QTableWidget): - def __init__(self, parent: QWidget): + def __init__(self, parent: QWidget, icon_cache: Cache): super(AppsTable, self).__init__() self.setParent(parent) self.window = parent @@ -44,7 +48,7 @@ class AppsTable(QTableWidget): 'manage_window.columns.latest_version', 'flatpak.info.branch', 'flatpak.info.arch', - 'flatpak.info.id', + 'flatpak.info.description', 'flatpak.info.origin', 'manage_window.columns.installed', 'manage_window.columns.update']] @@ -60,7 +64,8 @@ class AppsTable(QTableWidget): self.network_man = QNetworkAccessManager() 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 @@ -68,7 +73,7 @@ class AppsTable(QTableWidget): 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.setIcon(QIcon(resource.get_path('img/info.svg'))) action_info.triggered.connect(self._get_app_info) @@ -84,7 +89,7 @@ class AppsTable(QTableWidget): action_uninstall.triggered.connect(self._uninstall_app) 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.triggered.connect(self._downgrade_app) action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) @@ -99,25 +104,54 @@ class AppsTable(QTableWidget): menu_row.popup(QCursor.pos()) 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()] - def get_selected_app_icon(self): + def get_selected_app_icon(self) -> QIcon: return self.item(self.currentRow(), 0).icon() def _uninstall_app(self): selected_app = self.get_selected_app() if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], - body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), + 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): - self.window.uninstall_app(selected_app['model']['ref']) + self.window.uninstall_app(selected_app) def _downgrade_app(self): selected_app = self.get_selected_app() if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'], - body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']), + 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): self.window.downgrade_app(selected_app) @@ -132,94 +166,92 @@ class AppsTable(QTableWidget): def _load_icon(self, http_response): icon_url = http_response.url().toString() - pixmap = QPixmap() - pixmap.loadFromData(http_response.readAll()) - icon = QIcon(pixmap) - self.icon_cache[icon_url] = icon - for idx, app in enumerate(self.window.apps): - if app['model']['icon'] == icon_url: - self.item(idx, 0).setIcon(icon) - self.window.resize_and_center() - break + if not self.icon_cache.get(icon_url): + pixmap = QPixmap() + pixmap.loadFromData(http_response.readAll()) + icon = QIcon(pixmap) + 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.setRowCount(len(apps) if apps else 0) + self.setRowCount(len(app_views) if app_views else 0) - if apps: - for idx, app in enumerate(apps): - - tooltip = util.strip_html(app['model']['description']) if app['model']['description'] else None + if app_views: + for idx, app_v in enumerate(app_views): 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.setToolTip(tooltip) - if not app['model']['icon']: + if not app_v.model.base_data.icon_url: col_name.setIcon(self.icon_flathub) 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: col_name.setIcon(cached_icon) else: col_name.setIcon(self.icon_flathub) - self.network_man.get(QNetworkRequest(QUrl(app['model']['icon']))) self.setItem(idx, 0, col_name) col_version = QTableWidgetItem() - col_version.setText(app['model']['version']) + col_version.setText(app_v.model.base_data.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.setText(app_v.get_async_attr('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']: + 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_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.setToolTip(tooltip) self.setItem(idx, 3, col_branch) 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.setToolTip(tooltip) self.setItem(idx, 4, col_arch) - col_id = QTableWidgetItem() - col_id.setText(app['model']['id']) - col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - col_id.setToolTip(tooltip) - self.setItem(idx, 5, col_id) + desc = app_v.get_async_attr('description', strip_html=True) + col_description = QTableWidgetItem() + col_description.setText(desc) + col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + + if app_v.model.status == ApplicationStatus.READY: + col_description.setToolTip(desc) + + self.setItem(idx, 5, col_description) 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.setToolTip(tooltip) self.setItem(idx, 6, col_origin) col_installed = QLabel() - col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross'))))) - col_installed.setToolTip(tooltip) + col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app_v.model.installed else 'red_cross'))))) col_installed.setAlignment(Qt.AlignCenter) self.setCellWidget(idx, 7, col_installed) - col_update = UpdateToggleButton(app, self.window, self.window.locale_keys, app['model']['update']) if app['model']['update'] else None + 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) def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): header_horizontal = self.horizontalHeader() 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) diff --git a/fpakman/view/qt/history.py b/fpakman/view/qt/history.py index 8cb5877f..965928da 100644 --- a/fpakman/view/qt/history.py +++ b/fpakman/view/qt/history.py @@ -11,7 +11,7 @@ class HistoryDialog(QDialog): def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict): super(HistoryDialog, self).__init__() - self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name'])) + self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name)) self.setWindowIcon(app_icon) layout = QVBoxLayout() @@ -29,7 +29,7 @@ class HistoryDialog(QDialog): 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())): item = QTableWidgetItem() diff --git a/fpakman/view/qt/systray.py b/fpakman/view/qt/systray.py index a462531b..3b327bd1 100755 --- a/fpakman/view/qt/systray.py +++ b/fpakman/view/qt/systray.py @@ -1,4 +1,5 @@ import time +from threading import Lock from typing import List 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.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.window import ManageWindow @@ -26,7 +29,7 @@ class UpdateCheck(QThread): 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: self.signal.emit(updates) @@ -34,25 +37,13 @@ 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, 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__() self.locale_keys = locale_keys self.manager = manager + self.icon_cache = icon_cache self.icon_default = QIcon(resource.get_path('img/logo.png')) self.icon_update = QIcon(resource.get_path('img/logo_update.png')) @@ -60,12 +51,8 @@ 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) @@ -82,46 +69,45 @@ class TrayIcon(QSystemTrayIcon): self.dialog_about = None - self.thread_database = LoadDatabase(manager) - self.thread_database.signal_finished.connect(self._update_menu) self.last_updates = set() self.update_notification = update_notification + self.lock_notify = Lock() - def load_database(self): - self.thread_database.start() + def notify_updates(self, updates: List[Application]): - def _update_menu(self): - self.action_refreshing.setVisible(False) - self.action_manage.setVisible(True) + self.lock_notify.acquire() - 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): - self.last_updates = update_keys - msg = '{}: {}'.format(self.locale_keys['notification.new_updates'].format('Flatpak'), len(updates)) - self.setToolTip(msg) + if self.update_notification: + system.notify_user(msg) - if self.update_notification: - system.notify_user(msg) + else: + new_icon = self.icon_default + self.setToolTip(None) - else: - new_icon = self.icon_default - self.setToolTip(None) + if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed + self.setIcon(new_icon) - if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed - self.setIcon(new_icon) + finally: + self.lock_notify.release() def show_manage_window(self): if self.manage_window is None: self.manage_window = ManageWindow(locale_keys=self.locale_keys, manager=self.manager, + icon_cache=self.icon_cache, tray_icon=self) if self.manage_window.isMinimized(): diff --git a/fpakman/view/qt/thread.py b/fpakman/view/qt/thread.py index d289ab5c..ea4dbcb1 100644 --- a/fpakman/view/qt/thread.py +++ b/fpakman/view/qt/thread.py @@ -1,10 +1,15 @@ import time +from datetime import datetime, timedelta +from typing import List from PyQt5.QtCore import QThread, pyqtSignal from fpakman.core import flatpak 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.view_model import ApplicationView, ApplicationViewStatus class UpdateSelectedApps(QThread): @@ -43,17 +48,21 @@ class UninstallApp(QThread): signal_finished = pyqtSignal() signal_output = pyqtSignal(str) - def __init__(self): + def __init__(self, manager: FlatpakManager, icon_cache: Cache, app: ApplicationView = None): super(UninstallApp, self).__init__() - self.app_ref = None + self.app = app + self.manager = manager + self.icon_cache = icon_cache def run(self): - if self.app_ref: - for output in flatpak.uninstall_and_stream(self.app_ref): + if self.app and isinstance(self.app.model, FlatpakApplication): + for output in flatpak.uninstall_and_stream(self.app.model.ref): line = output.decode().strip() if 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() @@ -61,23 +70,23 @@ class DowngradeApp(QThread): signal_finished = pyqtSignal() 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__() self.manager = manager - self.app = None - self.root_password = None + self.app = app self.locale_keys = locale_keys + self.root_password = None 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: dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], body=self.locale_keys['popup.downgrade.impossible.body']) else: - for output in self.manager.downgrade_app(self.app['model'], self.root_password): + for output in self.manager.downgrade_app(self.app.model, self.root_password): line = output.decode().strip() if line: self.signal_output.emit(line) @@ -90,16 +99,16 @@ class DowngradeApp(QThread): class GetAppInfo(QThread): signal_finished = pyqtSignal(dict) - def __init__(self): + def __init__(self, app: ApplicationView = None): super(GetAppInfo, self).__init__() - self.app = None + self.app = app def run(self): - if self.app: - app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch']) - app_info['name'] = self.app['model']['name'] - app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app' - app_info['description'] = self.app['model']['description'] + if self.app and isinstance(self.app.model, FlatpakApplication): + app_info = flatpak.get_app_info_fields(self.app.model.base_data.id, self.app.model.branch) + app_info['name'] = self.app.model.base_data.name + app_info['type'] = 'runtime' if self.app.model.runtime else 'app' + app_info['description'] = self.app.model.base_data.description self.signal_finished.emit(app_info) self.app = None @@ -107,14 +116,14 @@ class GetAppInfo(QThread): class GetAppHistory(QThread): signal_finished = pyqtSignal(dict) - def __init__(self): + def __init__(self, app: ApplicationView = None): super(GetAppHistory, self).__init__() - self.app = None + self.app = app def run(self): - if self.app: - commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin']) - self.signal_finished.emit({'model': self.app['model'], 'commits': commits}) + if self.app and isinstance(self.app.model, FlatpakApplication): + 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 @@ -141,14 +150,14 @@ class InstallApp(QThread): signal_finished = pyqtSignal() signal_output = pyqtSignal(str) - def __init__(self): + def __init__(self, app: ApplicationView = None): super(InstallApp, self).__init__() - self.app = None + self.app = app def run(self): - if self.app: - for output in flatpak.install_and_stream(self.app['model']['id'], self.app['model']['origin']): + if self.app and isinstance(self.app.model, FlatpakApplication): + for output in flatpak.install_and_stream(self.app.model.base_data.id, self.app.model.origin): line = output.decode().strip() if line: self.signal_output.emit(line) @@ -184,3 +193,36 @@ class AnimateProgress(QThread): time.sleep(0.05) 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 diff --git a/fpakman/view/qt/view_model.py b/fpakman/view/qt/view_model.py new file mode 100644 index 00000000..6a090bf7 --- /dev/null +++ b/fpakman/view/qt/view_model.py @@ -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 diff --git a/fpakman/view/qt/window.py b/fpakman/view/qt/window.py index 955f30a1..8e23a1d5 100755 --- a/fpakman/view/qt/window.py +++ b/fpakman/view/qt/window.py @@ -10,13 +10,16 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHead from fpakman.core import resource, flatpak 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.apps_table import AppsTable from fpakman.view.qt.history import HistoryDialog from fpakman.view.qt.info import InfoDialog from fpakman.view.qt.root import is_root, ask_root_password from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ - GetAppHistory, SearchApps, InstallApp, AnimateProgress + GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels +from fpakman.view.qt.view_model import ApplicationView DARK_ORANGE = '#FF4500' @@ -24,7 +27,7 @@ DARK_ORANGE = '#FF4500' class ManageWindow(QWidget): __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__() self.locale_keys = locale_keys self.manager = manager @@ -33,6 +36,7 @@ class ManageWindow(QWidget): self.working = False # restrict the number of threaded actions self.apps = [] self.label_flatpak = None + self.icon_cache = icon_cache self.icon_flathub = QIcon(resource.get_path('img/logo.svg')) self._check_flatpak_installed() @@ -101,7 +105,7 @@ class ManageWindow(QWidget): self.layout.addWidget(toolbar) - self.table_apps = AppsTable(self) + self.table_apps = AppsTable(self, self.icon_cache) self.table_apps.change_headers_policy() self.layout.addWidget(self.table_apps) @@ -131,7 +135,7 @@ class ManageWindow(QWidget): self.thread_refresh = RefreshApps(self.manager) 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_finished.connect(self._finish_uninstall) @@ -155,6 +159,9 @@ class ManageWindow(QWidget): self.thread_animate_progress = AnimateProgress() 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.label_updates = QLabel('') self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) @@ -175,6 +182,9 @@ class ManageWindow(QWidget): self.centralize() + def _notify_model_data_change(self): + self.table_apps.fill_async_data() + def _new_spacer(self): spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -250,20 +260,20 @@ class ManageWindow(QWidget): self.thread_refresh.start() - def _finish_refresh(self, apps: List[dict]): + def _finish_refresh(self, apps: List[Application]): self.update_apps(apps) self.finish_action() self._release_lock() - def uninstall_app(self, app_ref: str): + def uninstall_app(self, app: ApplicationView): self._check_flatpak_installed() if self._acquire_lock(): self._handle_console_option(True) 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() def _finish_uninstall(self): @@ -281,10 +291,10 @@ class ManageWindow(QWidget): if self.apps: show_only_apps = True if only_apps == 2 else False - for idx, app in enumerate(self.apps): - hidden = show_only_apps and app['model']['runtime'] + for idx, app_v in enumerate(self.apps): + hidden = show_only_apps and isinstance(app_v.model, FlatpakApplication) and app_v.model.runtime self.table_apps.setRowHidden(idx, hidden) - app['visible'] = not hidden + app_v.visible = not hidden self.change_update_state() self.table_apps.change_headers_policy(QHeaderView.Stretch) @@ -297,9 +307,9 @@ class ManageWindow(QWidget): app_updates, runtime_updates = 0, 0 - for app in self.apps: - if app['model']['update']: - if app['model']['runtime']: + for app_v in self.apps: + if app_v.model.update: + if app_v.model.runtime: runtime_updates += 1 else: app_updates += 1 @@ -314,13 +324,13 @@ class ManageWindow(QWidget): else: self.label_updates.setText('') - for app in self.apps: - if app['visible'] and app['update_checked']: + for app_v in self.apps: + if app_v.visible and app_v.update_checked: enable_bt_update = True break 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): geo = self.frameGeometry() @@ -329,7 +339,7 @@ class ManageWindow(QWidget): geo.moveCenter(center_point) self.move(geo.topLeft()) - def update_apps(self, apps: List[dict]): + def update_apps(self, apps: List[Application]): self._check_flatpak_installed() self.apps = [] @@ -338,11 +348,10 @@ class ManageWindow(QWidget): if apps: for app in apps: - app_model = {'model': app, - 'update_checked': app['update'], - 'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()} + app_model = ApplicationView(model=app, + visible=(isinstance(app, FlatpakApplication) and 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) if napps == 0: @@ -357,9 +366,11 @@ class ManageWindow(QWidget): self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0) self.resize_and_center() + self.thread_verify_models.apps = self.apps + self.thread_verify_models.start() + def resize_and_center(self): - new_width = reduce(operator.add, - [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05 + new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05 self.resize(new_width, self.height()) self.centralize() @@ -368,7 +379,7 @@ class ManageWindow(QWidget): if self._acquire_lock(): 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: self._handle_console_option(True) @@ -390,7 +401,7 @@ class ManageWindow(QWidget): self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.ref_progress_bar.setVisible(True) - self.progress_bar.setValue(50) + self.label_status.setText(action_label + "...") self.toolbar_search.setVisible(False) self.bt_upgrade.setEnabled(False) @@ -398,12 +409,11 @@ class ManageWindow(QWidget): self.checkbox_only_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.thread_animate_progress.stop = True - self.ref_progress_bar.setVisible(False) self.progress_bar.setValue(0) - self.progress_bar.setVisible(False) self.bt_refresh.setEnabled(True) self.toolbar_search.setVisible(True) self.checkbox_only_apps.setEnabled(True) @@ -411,9 +421,6 @@ class ManageWindow(QWidget): 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() @@ -477,10 +484,10 @@ class ManageWindow(QWidget): self.thread_search.word = word self.thread_search.start() - def _finish_search(self, apps_found: List[dict]): + def _finish_search(self, apps_found: List[Application]): self._release_lock() - self.finish_action(clear_search=False) + self.finish_action() self.update_apps(apps_found) def install_app(self, app: dict):