mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 21:44:16 +02:00
improving initialization time and reducing async overload
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_())
|
||||
|
||||
2
fpakman/core/constants.py
Normal file
2
fpakman/core/constants.py
Normal file
@@ -0,0 +1,2 @@
|
||||
FLATHUB_URL = 'https://flathub.org'
|
||||
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@ import subprocess
|
||||
from typing import List
|
||||
|
||||
from fpakman.core import system
|
||||
from fpakman.core.model import Application
|
||||
|
||||
BASE_CMD = 'flatpak'
|
||||
|
||||
|
||||
42
fpakman/core/model.py
Normal file
42
fpakman/core/model.py
Normal 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
119
fpakman/core/worker.py
Normal 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
0
fpakman/util/__init__.py
Normal file
50
fpakman/util/cache.py
Normal file
50
fpakman/util/cache.py
Normal 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
25
fpakman/util/memory.py
Normal 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
27
fpakman/view/qt/view_model.py
Normal file
27
fpakman/view/qt/view_model.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user