This commit is contained in:
Vinícius Moreira
2019-07-13 13:01:40 -03:00
committed by GitHub
27 changed files with 1390 additions and 473 deletions

View File

@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.3.1] - 2019-07-13
### Improvements
- Console output now is optional and not shown by default
- Search bar is cleaned when 'Refresh' is clicked
- Full Flatpak database is not loaded during initialization: speeds up the process and reduces memory usage
- Applications data not available offline are now retrieved from Flathub API on demand and cached in memory and disk (only installed)
- In-memory cached data have an expiration time and are cleaned overtime to reduce memory usage
- Code was refactored to support other types of packaging in the future (e.g: snap)
- flatpak is not a requirement anymore
- the amount of columns of the applications table was reduced to improve the user experience
- new environment variables and arguments: FPAKMAN_ICON_EXPIRATION (--icon-exp), FPAKMAN_DISK_CACHE (--disk-cache)
- minor GUI improvements
### Fixes:
- flatpak 1.0.X: search is now retrieving the application name
- app crashes when there is no internet connection while initializing, downgrading or retrieving app history
## [0.3.0] - 2019-07-02 ## [0.3.0] - 2019-07-02
### Features ### Features
- Applications search - Applications search

View File

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

View File

@@ -1 +1,2 @@
__version__ = '0.3.0' __version__ = '0.3.1'
__app_name__ = 'fpakman'

View File

@@ -6,34 +6,41 @@ from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from colorama import Fore from colorama import Fore
from fpakman import __version__ from fpakman import __version__, __app_name__
from fpakman.core import resource from fpakman.core import resource
from fpakman.core import util from fpakman.core.disk import DiskCacheLoaderFactory
from fpakman.core.controller import FlatpakManager from fpakman.core.structure import prepare_folder_structure
from fpakman.util import util
from fpakman.core.controller import FlatpakManager, GenericApplicationManager
from fpakman.util.cache import Cache
from fpakman.util.memory import CacheCleaner
from fpakman.view.qt.systray import TrayIcon from fpakman.view.qt.systray import TrayIcon
app_name = 'fpakman'
def log_msg(msg: str, color: int = None): def log_msg(msg: str, color: int = None):
if color is None: if color is None:
print('[{}] {}'.format(app_name, msg)) print('[{}] {}'.format(__app_name__, msg))
else: else:
print('{}[{}] {}{}'.format(color, app_name, msg, Fore.RESET)) print('{}[{}] {}{}'.format(color, __app_name__, msg, Fore.RESET))
parser = argparse.ArgumentParser(prog=app_name, description="GUI for Flatpak applications management") parser = argparse.ArgumentParser(prog=__app_name__, description="GUI for Flatpak applications management")
parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__))
parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached application expiration time in SECONDS. Default: %(default)s') parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached API data expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('FPAKMAN_ICON_EXPIRATION', 60 * 5)), type=int, help='cached icons expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s') parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s')
parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s') parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s')
parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enable/disable system notifications for new updates. Default: %(default)s') parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enables / disables system notifications for new updates. Default: %(default)s')
parser.add_argument('-dc', '--disk-cache', action="store", default=os.getenv('FPAKMAN_DISK_CACHE', 1), type=int, help='Enables / disables disk cache. When disk cache is enabled, the installed applications data are loaded faster. Default: %(default)s')
args = parser.parse_args() args = parser.parse_args()
if args.cache_exp <= 0: if args.cache_exp < 0:
log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW) log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if args.icon_exp < 0:
log_msg("'icon-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if not args.locale.strip(): if not args.locale.strip():
log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED) log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED)
exit(1) exit(1)
@@ -47,16 +54,30 @@ if args.update_notification == 0:
locale_keys = util.get_locale_keys(args.locale) locale_keys = util.get_locale_keys(args.locale)
prepare_folder_structure(disk_cache=args.disk_cache)
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)
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg'))) app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
manager = FlatpakManager(cache_expire=args.cache_exp) disk_loader_factory = DiskCacheLoaderFactory(disk_cache=args.disk_cache, flatpak_api_cache=flatpak_api_cache)
manager = GenericApplicationManager([FlatpakManager(flatpak_api_cache, disk_cache=args.disk_cache)], disk_loader_factory=disk_loader_factory)
trayIcon = TrayIcon(locale_keys=locale_keys, trayIcon = TrayIcon(locale_keys=locale_keys,
manager=manager, manager=manager,
check_interval=args.check_interval, check_interval=args.check_interval,
icon_cache=icon_cache,
disk_cache=args.disk_cache,
update_notification=bool(args.update_notification)) update_notification=bool(args.update_notification))
trayIcon.load_database()
trayIcon.show() trayIcon.show()
CacheCleaner(caches).start()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

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

View File

@@ -1,106 +1,145 @@
from datetime import datetime, timedelta import os
import shutil
from abc import ABC, abstractmethod
from datetime import datetime
from threading import Lock from threading import Lock
from typing import List from typing import List
import requests import requests
from fpakman.core import flatpak from fpakman.core import flatpak, disk
from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory
__FLATHUB_URL__ = 'https://flathub.org' from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus, Application
__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' from fpakman.util.cache import Cache
class FlatpakManager: class ApplicationManager(ABC):
def __init__(self, cache_expire: int = 60 * 60): @abstractmethod
self.cache_apps = {} def search(self, word: str, disk_loader: DiskCacheLoader) -> List[Application]:
self.cache_expire = cache_expire pass
@abstractmethod
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool) -> List[Application]:
pass
@abstractmethod
def downgrade_app(self, app: Application, root_password: str):
pass
@abstractmethod
def clean_cache_for(self, app: Application):
pass
@abstractmethod
def can_downgrade(self):
pass
@abstractmethod
def update_and_stream(self, app: Application):
pass
@abstractmethod
def uninstall_and_stream(self, app: Application):
pass
@abstractmethod
def get_app_type(self) -> str:
pass
@abstractmethod
def get_info(self, app: Application) -> dict:
pass
@abstractmethod
def get_history(self, app: Application) -> List[dict]:
pass
@abstractmethod
def install_and_stream(self, app: Application):
pass
@abstractmethod
def is_enabled(self) -> bool:
pass
@abstractmethod
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
pass
from fpakman.core.worker import FlatpakAsyncDataLoaderManager
class FlatpakManager(ApplicationManager):
def __init__(self, api_cache: Cache, disk_cache: bool):
self.api_cache = api_cache
self.http_session = requests.Session() self.http_session = requests.Session()
self.lock_db_read = Lock()
self.lock_read = Lock() self.lock_read = Lock()
self.disk_cache = disk_cache
self.async_data_loader = FlatpakAsyncDataLoaderManager(api_cache=self.api_cache, manager=self)
flatpak.set_default_remotes()
def load_full_database(self): def get_app_type(self):
return FlatpakApplication
self.lock_db_read.acquire() def _map_to_model(self, app: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
try: model = FlatpakApplication(arch=app.get('arch'),
res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30) 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')))
model.installed = installed
if res.status_code == 200: api_data = self.api_cache.get(app['id'])
for app in res.json():
self.cache_apps[app['flatpakAppId']] = app
finally:
self.lock_db_read.release()
def _request_app_data(self, app_id: str): expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
try: if not api_data or expired_data:
res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30) if not app['runtime']:
disk_loader.add(model) # preloading cached disk data
model.status = ApplicationStatus.LOADING_DATA
self.async_data_loader.load(model)
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
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: else:
app['latest_version'] = api_data.get('currentReleaseVersion') model.fill_cached_data(api_data)
app['icon'] = api_data.get('iconMobileUrl')
for attr in ('name', 'description'): return model
if not app.get(attr):
app[attr] = api_data.get(attr)
if app['icon'].startswith('/'): def search(self, word: str, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
app['icon'] = __FLATHUB_URL__ + app['icon']
def search(self, word: str) -> List[dict]:
res = [] res = []
apps_found = flatpak.search(word) apps_found = flatpak.search(word)
if apps_found: if apps_found:
already_read = set() already_read = set()
installed_apps = self.read_installed() installed_apps = self.read_installed(disk_loader=disk_loader, keep_workers=True)
if installed_apps: if installed_apps:
for app in apps_found: for app_found in apps_found:
for installed_app in installed_apps: for installed_app in installed_apps:
if app['id'] == installed_app['id']: if app_found['id'] == installed_app.base_data.id:
res.append(installed_app) res.append(installed_app)
already_read.add(app['id']) already_read.add(app_found['id'])
for app in apps_found: for app_found in apps_found:
if app['id'] not in already_read: if app_found['id'] not in already_read:
app['update'] = False res.append(self._map_to_model(app_found, False, disk_loader))
app['installed'] = False
self._fill_api_data(app) disk_loader.stop = True
res.append(app) disk_loader.join()
self.async_data_loader.stop_current_workers()
return res return res
def read_installed(self) -> List[dict]: def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool = False) -> List[FlatpakApplication]:
self.lock_read.acquire() self.lock_read.acquire()
@@ -112,23 +151,166 @@ class FlatpakManager:
available_updates = flatpak.list_updates_as_str() available_updates = flatpak.list_updates_as_str()
for app in installed: models = []
self._fill_api_data(app)
app['update'] = app['id'] in available_updates
app['installed'] = True
return installed for app in installed:
model = self._map_to_model(app, True, disk_loader)
model.update = app['id'] in available_updates
models.append(model)
if not keep_workers:
self.async_data_loader.stop_current_workers()
return models
return []
finally: finally:
self.lock_read.release() self.lock_read.release()
def downgrade_app(self, app: dict, root_password: str): def can_downgrade(self):
return True
commits = flatpak.get_app_commits(app['ref'], app['origin']) def downgrade_app(self, app: FlatpakApplication, root_password: str):
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: # downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1: if commit_idx == len(commits) - 1:
return None return None
return flatpak.downgrade_and_stream(app['ref'], commits[commit_idx + 1], root_password) return flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password)
def clean_cache_for(self, app: FlatpakApplication):
self.api_cache.delete(app.base_data.id)
if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()):
shutil.rmtree(app.get_disk_cache_path())
def update_and_stream(self, app: FlatpakApplication):
return flatpak.update_and_stream(app.ref)
def uninstall_and_stream(self, app: FlatpakApplication):
return flatpak.uninstall_and_stream(app.ref)
def get_info(self, app: FlatpakApplication) -> dict:
app_info = flatpak.get_app_info_fields(app.base_data.id, app.branch)
app_info['name'] = app.base_data.name
app_info['type'] = 'runtime' if app.runtime else 'app'
app_info['description'] = app.base_data.description
return app_info
def get_history(self, app: FlatpakApplication) -> List[dict]:
return flatpak.get_app_commits_data(app.ref, app.origin)
def install_and_stream(self, app: FlatpakApplication):
return flatpak.install_and_stream(app.base_data.id, app.origin)
def is_enabled(self):
return flatpak.is_installed()
def cache_to_disk(self, app: FlatpakApplication, icon_bytes: bytes, only_icon: bool):
if self.disk_cache and app.supports_disk_cache():
disk.save(app, icon_bytes, only_icon)
class GenericApplicationManager(ApplicationManager):
def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory):
self.managers = managers
self.map = {m.get_app_type(): m for m in self.managers}
self.disk_loader_factory = disk_loader_factory
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> List[Application]:
apps = []
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
for man in self.managers:
if man.is_enabled():
apps.extend(man.search(word, disk_loader))
disk_loader.stop = True
disk_loader.join()
return apps
def read_installed(self, disk_loader: DiskCacheLoader = None, keep_workers: bool = False) -> List[Application]:
installed = []
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
for man in self.managers:
if man.is_enabled():
installed.extend(man.read_installed(disk_loader=disk_loader, keep_workers=keep_workers))
disk_loader.stop = True
disk_loader.join()
return installed
def can_downgrade(self):
return True
def downgrade_app(self, app: Application, root_password: str):
man = self._get_manager_for(app)
if man and man.can_downgrade():
return man.downgrade_app(app, root_password)
else:
raise Exception("downgrade is not possible for {}".format(app.__class__.__name__))
def clean_cache_for(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.clean_cache_for(app)
def update_and_stream(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.update_and_stream(app)
def uninstall_and_stream(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.uninstall_and_stream(app)
def install_and_stream(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.install_and_stream(app)
def get_info(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.get_info(app)
def get_history(self, app: Application):
man = self._get_manager_for(app)
if man:
return man.get_history(app)
def get_app_type(self):
return None
def is_enabled(self):
return True
def _get_manager_for(self, app: Application) -> ApplicationManager:
man = self.map[app.__class__]
return man if man and man.is_enabled() else None
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
if self.disk_loader_factory.disk_cache and app.supports_disk_cache():
man = self._get_manager_for(app)
if man:
return man.cache_to_disk(app, icon_bytes=icon_bytes, only_icon=only_icon)

79
fpakman/core/disk.py Normal file
View File

@@ -0,0 +1,79 @@
import json
import os
from pathlib import Path
from threading import Thread, Lock
from typing import List
from fpakman.core.model import Application, FlatpakApplication
from fpakman.util.cache import Cache
class DiskCacheLoader(Thread):
def __init__(self, enabled: bool, flatpak_api_cache: Cache, apps: List[Application] = []):
super(DiskCacheLoader, self).__init__(daemon=True)
self.apps = apps
self.stop = False
self.lock = Lock()
self.flatpak_api_cache = flatpak_api_cache
self.enabled = enabled
def run(self):
if self.enabled:
while True:
if self.apps:
self.lock.acquire()
app = self.apps[0]
del self.apps[0]
self.lock.release()
self.fill_cached_data(app)
elif self.stop:
break
def add(self, app: Application):
if self.enabled:
if app and app.supports_disk_cache():
self.lock.acquire()
self.apps.append(app)
self.lock.release()
def fill_cached_data(self, app: Application):
if self.enabled:
if os.path.exists(app.get_disk_data_path()):
with open(app.get_disk_data_path()) as f:
cached_data = json.loads(f.read())
app.fill_cached_data(cached_data)
if isinstance(app, FlatpakApplication):
self.flatpak_api_cache.add_non_existing(app.base_data.id, cached_data)
class DiskCacheLoaderFactory:
def __init__(self, disk_cache: bool, flatpak_api_cache: Cache):
self.disk_cache = disk_cache
self.flatpak_api_cache = flatpak_api_cache
def new(self):
return DiskCacheLoader(enabled=self.disk_cache, flatpak_api_cache=self.flatpak_api_cache)
def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False):
if app.supports_disk_cache():
if not only_icon:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
if isinstance(app, FlatpakApplication):
data = app.get_data_to_cache()
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(data))
if icon_bytes:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
with open(app.get_disk_icon_path(), 'wb+') as f:
f.write(icon_bytes)

View File

@@ -0,0 +1,3 @@
class NoInternetException(Exception):
pass

View File

@@ -3,6 +3,8 @@ import subprocess
from typing import List from typing import List
from fpakman.core import system from fpakman.core import system
from fpakman.core.exception import NoInternetException
from fpakman.core.model import Application
BASE_CMD = 'flatpak' BASE_CMD = 'flatpak'
@@ -72,7 +74,7 @@ def is_installed():
def get_version(): def get_version():
res = system.run_cmd('{} --version'.format(BASE_CMD)) res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False)
return res.split(' ')[1].strip() if res else None return res.split(' ')[1].strip() if res else None
@@ -97,7 +99,7 @@ def update_and_stream(app_ref: str):
:param app_ref: :param app_ref:
:return: :return:
""" """
return system.stream_cmd([BASE_CMD, 'update', '-y', app_ref]) return system.cmd_to_subprocess([BASE_CMD, 'update', '-y', app_ref])
def uninstall_and_stream(app_ref: str): def uninstall_and_stream(app_ref: str):
@@ -106,7 +108,7 @@ def uninstall_and_stream(app_ref: str):
:param app_ref: :param app_ref:
:return: :return:
""" """
return system.stream_cmd([BASE_CMD, 'uninstall', app_ref, '-y']) return system.cmd_to_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
def list_updates_as_str(): def list_updates_as_str():
@@ -127,12 +129,19 @@ def downgrade_and_stream(app_ref: str, commit: str, root_password: str):
def get_app_commits(app_ref: str, origin: str) -> List[str]: def get_app_commits(app_ref: str, origin: str) -> List[str]:
log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
return re.findall(r'Commit+:\s(.+)', log)
if log:
return re.findall(r'Commit+:\s(.+)', log)
else:
raise NoInternetException()
def get_app_commits_data(app_ref: str, origin: str) -> List[dict]: def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
if not log:
raise NoInternetException()
res = re.findall(r'(Commit|Subject|Date):\s(.+)', log) res = re.findall(r'(Commit|Subject|Date):\s(.+)', log)
commits = [] commits = []
@@ -210,4 +219,9 @@ def search(word: str) -> List[dict]:
def install_and_stream(app_id: str, origin: str): def install_and_stream(app_id: str, origin: str):
return system.stream_cmd([BASE_CMD, 'install', origin, app_id, '-y']) return system.cmd_to_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
def set_default_remotes():
system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo')

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

@@ -0,0 +1,150 @@
from abc import ABC, abstractmethod
from enum import Enum
from fpakman.core import resource
from fpakman.core.structure import flatpak_cache_path
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
@abstractmethod
def has_history(self):
pass
@abstractmethod
def has_info(self):
pass
@abstractmethod
def can_be_downgraded(self):
pass
@abstractmethod
def can_be_uninstalled(self):
pass
@abstractmethod
def can_be_installed(self):
pass
@abstractmethod
def get_type(self):
pass
def get_default_icon_path(self):
return resource.get_path('img/logo.svg')
@abstractmethod
def is_library(self):
pass
@abstractmethod
def supports_disk_cache(self):
pass
@abstractmethod
def get_disk_cache_path(self):
pass
@abstractmethod
def get_disk_icon_path(self):
pass
@abstractmethod
def get_disk_data_path(self):
pass
@abstractmethod
def get_data_to_cache(self):
pass
@abstractmethod
def fill_cached_data(self, data: dict):
pass
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
def has_history(self):
return True
def has_info(self):
return True
def can_be_downgraded(self):
return not self.runtime
def can_be_uninstalled(self):
return True
def can_be_installed(self):
return True
def get_type(self):
return 'flatpak'
def get_default_icon_path(self):
return resource.get_path('img/flathub.svg')
def is_library(self):
return self.runtime
def supports_disk_cache(self):
return self.installed and not self.is_library()
def get_disk_cache_path(self):
return '{}/{}'.format(flatpak_cache_path, self.base_data.id)
def get_disk_icon_path(self):
return '{}/icon.png'.format(self.get_disk_cache_path())
def get_disk_data_path(self):
return '{}/data.json'.format(self.get_disk_cache_path())
def get_data_to_cache(self):
return {
'description': self.base_data.description,
'icon_url': self.base_data.icon_url,
'latest_version': self.base_data.latest_version,
'version': self.base_data.version,
'name': self.base_data.name
}
def fill_cached_data(self, data: dict):
for attr in self.get_data_to_cache().keys():
if not getattr(self.base_data, attr):
setattr(self.base_data, attr, data[attr])

12
fpakman/core/structure.py Normal file
View File

@@ -0,0 +1,12 @@
from pathlib import Path
from fpakman import __app_name__
home_path = Path.home()
cache_path = '{}/.cache/{}'.format(home_path, __app_name__)
flatpak_cache_path = '{}/flatpak/installed'.format(cache_path)
def prepare_folder_structure(disk_cache: bool):
if disk_cache:
Path(flatpak_cache_path).mkdir(parents=True, exist_ok=True)

View File

@@ -5,13 +5,26 @@ from typing import List
from fpakman.core import resource from fpakman.core import resource
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False) -> str: def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str:
res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, env={'LANG': 'en'}) args = {
"shell": True,
"stdout": subprocess.PIPE,
"env": {'LANG': 'en'}
}
if not print_error:
args["stderr"] = subprocess.DEVNULL
res = subprocess.run(cmd, **args)
return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None
def stream_cmd(cmd: List[str]): def stream_cmd(cmd: List[str]):
return subprocess.Popen(cmd, stdout=subprocess.PIPE, env={'LANG': 'en'}).stdout return cmd_to_subprocess(cmd).stdout
def cmd_to_subprocess(cmd: List[str]):
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'LANG': 'en'})
def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')): def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')):

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

@@ -0,0 +1,140 @@
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.controller import ApplicationManager
from fpakman.core.model import FlatpakApplication, ApplicationStatus
from fpakman.util.cache import Cache
class FlatpakAsyncDataLoader(Thread):
def __init__(self, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 3, apps: List[FlatpakApplication] = []):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.apps = 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
self.to_persist = {} # stores all data loaded by the instance
self.manager = manager
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 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')
if not app.base_data.name:
app.base_data.name = data.get('name')
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 not app.base_data.version and app.base_data.latest_version:
app.base_data.version = app.base_data.latest_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
loaded_data = app.get_data_to_cache()
self.api_cache.add(app.base_data.id, loaded_data)
app.status = ApplicationStatus.READY
if app.supports_disk_cache():
self.to_persist[app.base_data.id] = app
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()
if self.apps:
del self.apps[0]
elif self.stop:
self.cache_to_disk()
break # stop working
def add(self, app: FlatpakApplication):
self.apps.append(app)
def current_load(self):
return len(self.apps)
def cache_to_disk(self):
if self.to_persist:
for app in self.to_persist.values():
self.manager.cache_to_disk(app=app, icon_bytes=None, only_icon=False)
self.to_persist = {}
class FlatpakAsyncDataLoaderManager:
def __init__(self, manager: ApplicationManager, api_cache: Cache, worker_load: int = 1, workers: List[FlatpakAsyncDataLoader] = []):
self.worker_load = worker_load
self.current_workers = workers
self.http_session = requests.Session()
self.api_cache = api_cache
self.manager = manager
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,
manager=self.manager)
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 = []

View File

@@ -1,6 +1,5 @@
manage_window.title=Flatpak Application Manager manage_window.title=Applications Manager
manage_window.columns.latest_version=Latest Version manage_window.columns.latest_version=Latest Version
manage_window.columns.installed=Installed
manage_window.columns.update=Upgrade ? manage_window.columns.update=Upgrade ?
manage_window.apps_table.row.actions.info=Information manage_window.apps_table.row.actions.info=Information
manage_window.apps_table.row.actions.history=History manage_window.apps_table.row.actions.history=History
@@ -25,8 +24,7 @@ manage_window.status.searching=Searching
manage_window.status.installing=Installing manage_window.status.installing=Installing
manage_window.bt.refresh.tooltip=Reload the data about installed applications manage_window.bt.refresh.tooltip=Reload the data about installed applications
manage_window.bt.upgrade.tooltip=Upgrade all selected applications manage_window.bt.upgrade.tooltip=Upgrade all selected applications
popup.flatpak_not_installed.title=Error manage_window.checkbox.show_details=Show details
popup.flatpak_not_installed.msg=flatpak seems not to be installed. Exiting
popup.root.title=Requires root privileges popup.root.title=Requires root privileges
popup.root.password=Password popup.root.password=Password
popup.root.bad_password.title=Error popup.root.bad_password.title=Error
@@ -42,7 +40,7 @@ tray.action.manage=Manage applications
tray.action.exit=Exit tray.action.exit=Exit
tray.action.about=About tray.action.about=About
tray.action.refreshing=Refreshing tray.action.refreshing=Refreshing
notification.new_updates={} updates notification.new_updates=Updates
flatpak.info.arch=arch flatpak.info.arch=arch
flatpak.info.branch=branch flatpak.info.branch=branch
flatpak.info.collection=collection flatpak.info.collection=collection
@@ -69,4 +67,12 @@ about.info.rate=If this tool is useful for you, give it a star on Github to keep
yes=yes yes=yes
no=no no=no
version.updated=updated version.updated=updated
version.outdated=outdated version.outdated=outdated
name=name
version=version
latest_version=latest version
description=description
type=type
installed=installed
others=others
internet.required=Internet connection is required

View File

@@ -1,4 +1,4 @@
manage_window.title=Administrador de Aplicativos Flatpak manage_window.title=Administrador de Aplicativos
manage_window.columns.latest_version=Ultima Versión manage_window.columns.latest_version=Ultima Versión
manage_window.columns.update=Actualizar ? manage_window.columns.update=Actualizar ?
manage_window.columns.installed=Instalado manage_window.columns.installed=Instalado
@@ -25,8 +25,7 @@ manage_window.status.searching=Buscando
manage_window.status.installing=Instalando manage_window.status.installing=Instalando
manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados
manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados
popup.flatpak_not_installed.title=Error manage_window.checkbox.show_details=Mostrar detalles
popup.flatpak_not_installed.msg=flatpak no parece estar instalado. Saliendo
popup.root.title=Requiere privilegios de root popup.root.title=Requiere privilegios de root
popup.root.password=Contraseña popup.root.password=Contraseña
popup.root.bad_password.title=Error popup.root.bad_password.title=Error
@@ -42,7 +41,7 @@ tray.action.manage=Administrar aplicativos
tray.action.exit=Salir tray.action.exit=Salir
tray.action.about=Sobre tray.action.about=Sobre
tray.action.refreshing=Recargando tray.action.refreshing=Recargando
notification.new_updates=Actualizaciones {} notification.new_updates=Actualizaciones
flatpak.info.arch=arquitectura flatpak.info.arch=arquitectura
flatpak.info.branch=rama flatpak.info.branch=rama
flatpak.info.collection=colección flatpak.info.collection=colección
@@ -69,4 +68,12 @@ about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Githu
yes=sí yes=sí
no=no no=no
version.updated=actualizada version.updated=actualizada
version.outdated=desactualizada version.outdated=desactualizada
name=nombre
version=versión
latest_version=ultima versión
description=descripción
type=tipo
installed=instalado
others=otros
internet.required=Se requiere conexión a internet

View File

@@ -1,4 +1,4 @@
manage_window.title=Gerenciador de Aplicativos Flatpak manage_window.title=Gerenciador de Aplicativos
manage_window.columns.latest_version=Última Versão manage_window.columns.latest_version=Última Versão
manage_window.columns.update=Atualizar ? manage_window.columns.update=Atualizar ?
manage_window.columns.installed=Instalado manage_window.columns.installed=Instalado
@@ -25,8 +25,7 @@ manage_window.status.searching=Buscando
manage_window.status.installing=Instalando manage_window.status.installing=Instalando
manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados
manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados
popup.flatpak_not_installed.title=Erro manage_window.checkbox.show_details=Mostrar detalhes
popup.flatpak_not_installed.msg=flatpak não parece estar instalado. Saindo
popup.root.title=Requer privilégios de root popup.root.title=Requer privilégios de root
popup.root.password=Senha popup.root.password=Senha
popup.root.bad_password.title=Erro popup.root.bad_password.title=Erro
@@ -42,7 +41,7 @@ tray.action.manage=Gerenciar aplicativos
tray.action.exit=Sair tray.action.exit=Sair
tray.action.about=Sobre tray.action.about=Sobre
tray.action.refreshing=Recarregando tray.action.refreshing=Recarregando
notification.new_updates=Atualizações {} notification.new_updates=Atualizações
flatpak.info.arch=arquitetura flatpak.info.arch=arquitetura
flatpak.info.branch=ramo flatpak.info.branch=ramo
flatpak.info.collection=coleção flatpak.info.collection=coleção
@@ -69,4 +68,12 @@ about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Githu
yes=sim yes=sim
no=não no=não
version.updated=atualizada version.updated=atualizada
version.outdated=desatualizada version.outdated=desatualizada
name=nome
version=versão
latest_version=última versão
description=descrição
type=tipo
installed=instalado
others=outros
internet.required=É necessário estar conectado a Internet

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

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

@@ -0,0 +1,63 @@
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 key and self.is_enabled():
self.lock.acquire()
self._add(key, val)
self.lock.release()
def _add(self, key: str, val: object):
self._cache[key] = {'val': val, 'expires_at': datetime.utcnow() + timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
def add_non_existing(self, key: str, val: object):
if key and self. is_enabled():
self.lock.acquire()
cur_val = self.get(key)
if cur_val is None:
self._add(key, val)
self.lock.release()
def get(self, key: str):
if key and 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 key and self.is_enabled():
if key in self._cache:
self.lock.acquire()
del self._cache[key]
self.lock.release()
def keys(self):
return set(self._cache.keys()) if self.is_enabled() else set()
def clean_expired(self):
if self.is_enabled():
for key in self.keys():
self.get(key)

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

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

View File

@@ -2,7 +2,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel
from fpakman import __version__ from fpakman import __version__, __app_name__
from fpakman.core import resource from fpakman.core import resource
PROJECT_URL = 'https://github.com/vinifmor/fpakman' PROJECT_URL = 'https://github.com/vinifmor/fpakman'
@@ -22,7 +22,7 @@ class AboutDialog(QDialog):
label_logo.setAlignment(Qt.AlignCenter) label_logo.setAlignment(Qt.AlignCenter)
layout.addWidget(label_logo) layout.addWidget(label_logo)
label_name = QLabel('fpakman ( {} {} )'.format(locale_keys['flatpak.info.version'].lower(), __version__)) label_name = QLabel('{} ( {} {} )'.format(__app_name__, locale_keys['flatpak.info.version'].lower(), __version__))
label_name.setStyleSheet('font-weight: bold;') label_name.setStyleSheet('font-weight: bold;')
label_name.setAlignment(Qt.AlignCenter) label_name.setAlignment(Qt.AlignCenter)
layout.addWidget(label_name) layout.addWidget(label_name)

View File

@@ -1,20 +1,25 @@
import os
from threading import Lock
from typing import List from typing import List
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel QHeaderView, QLabel
from fpakman.core import resource, util from fpakman.core import resource
from fpakman.core.model import ApplicationStatus
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView, ApplicationViewStatus
class UpdateToggleButton(QToolButton): class UpdateToggleButton(QToolButton):
def __init__(self, model: dict, root: QWidget, locale_keys: dict, checked: bool = True): def __init__(self, app_view: ApplicationView, root: QWidget, locale_keys: dict, checked: bool = True):
super(UpdateToggleButton, self).__init__() super(UpdateToggleButton, self).__init__()
self.app = model self.app_view = app_view
self.root = root self.root = root
self.setCheckable(True) self.setCheckable(True)
self.clicked.connect(self.change_state) self.clicked.connect(self.change_state)
@@ -28,25 +33,24 @@ class UpdateToggleButton(QToolButton):
self.click() self.click()
def change_state(self, not_checked: bool): def change_state(self, not_checked: bool):
self.app['update_checked'] = not not_checked self.app_view.update_checked = not not_checked
self.setIcon(self.icon_on if not not_checked else self.icon_off) self.setIcon(self.icon_on if not not_checked else self.icon_off)
self.root.change_update_state() self.root.change_update_state()
class AppsTable(QTableWidget): class AppsTable(QTableWidget):
def __init__(self, parent: QWidget): def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool):
super(AppsTable, self).__init__() super(AppsTable, self).__init__()
self.setParent(parent) self.setParent(parent)
self.window = parent self.window = parent
self.column_names = [parent.locale_keys[key].capitalize() for key in ['flatpak.info.name', self.disk_cache = disk_cache
'flatpak.info.version', self.column_names = [parent.locale_keys[key].capitalize() for key in ['name',
'manage_window.columns.latest_version', 'version',
'flatpak.info.branch', 'latest_version',
'flatpak.info.arch', 'flatpak.info.description',
'flatpak.info.id', 'type',
'flatpak.info.origin', 'installed',
'manage_window.columns.installed',
'manage_window.columns.update']] 'manage_window.columns.update']]
self.setColumnCount(len(self.column_names)) self.setColumnCount(len(self.column_names))
self.setFocusPolicy(Qt.NoFocus) self.setFocusPolicy(Qt.NoFocus)
@@ -56,11 +60,13 @@ class AppsTable(QTableWidget):
self.setHorizontalHeaderLabels(self.column_names) self.setHorizontalHeaderLabels(self.column_names)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.icon_flathub = QIcon(resource.get_path('img/flathub.svg')) self.icon_flathub = QIcon(resource.get_path('img/flathub.svg'))
self.icon_logo = QIcon(resource.get_path('img/logo.svg'))
self.network_man = QNetworkAccessManager() self.network_man = QNetworkAccessManager()
self.network_man.finished.connect(self._load_icon) self.network_man.finished.connect(self._load_icon_and_cache)
self.icon_cache = {} self.icon_cache = icon_cache
self.lock_async_data = Lock()
def contextMenuEvent(self, QContextMenuEvent): # selected row right click event def contextMenuEvent(self, QContextMenuEvent): # selected row right click event
@@ -68,56 +74,85 @@ class AppsTable(QTableWidget):
menu_row = QMenu() menu_row = QMenu()
if app['model']['installed']: if app.model.installed:
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
action_info.triggered.connect(self._get_app_info)
menu_row.addAction(action_info)
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"]) if app.model.has_info():
action_history.setIcon(QIcon(resource.get_path('img/history.svg'))) action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
action_history.triggered.connect(self._get_app_history) action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
menu_row.addAction(action_history) action_info.triggered.connect(self._get_app_info)
menu_row.addAction(action_info)
action_uninstall = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.uninstall"]) if app.model.has_history():
action_uninstall.setIcon(QIcon(resource.get_path('img/uninstall.svg'))) action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
action_uninstall.triggered.connect(self._uninstall_app) action_history.setIcon(QIcon(resource.get_path('img/history.svg')))
menu_row.addAction(action_uninstall) action_history.triggered.connect(self._get_app_history)
menu_row.addAction(action_history)
if not app['model']['runtime']: # not available for runtimes if app.model.can_be_uninstalled():
action_uninstall = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.uninstall"])
action_uninstall.setIcon(QIcon(resource.get_path('img/uninstall.svg')))
action_uninstall.triggered.connect(self._uninstall_app)
menu_row.addAction(action_uninstall)
if app.model.can_be_downgraded():
action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"]) action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"])
action_downgrade.triggered.connect(self._downgrade_app) action_downgrade.triggered.connect(self._downgrade_app)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
menu_row.addAction(action_downgrade) menu_row.addAction(action_downgrade)
else: else:
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"]) if app.model.can_be_installed():
action_install.setIcon(QIcon(resource.get_path('img/install.svg'))) action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
action_install.triggered.connect(self._install_app) action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
menu_row.addAction(action_install) action_install.triggered.connect(self._install_app)
menu_row.addAction(action_install)
menu_row.adjustSize() menu_row.adjustSize()
menu_row.popup(QCursor.pos()) menu_row.popup(QCursor.pos())
menu_row.exec_() menu_row.exec_()
def get_selected_app(self): def fill_async_data(self):
self.lock_async_data.acquire()
if self.window.apps:
for idx, app_v in enumerate(self.window.apps):
if app_v.visible and app_v.status == ApplicationViewStatus.LOADING and app_v.model.status == ApplicationStatus.READY:
self.network_man.get(QNetworkRequest(QUrl(app_v.model.base_data.icon_url)))
app_name = self.item(idx, 0).text()
if not app_name or app_name == '...':
self.item(idx, 0).setText(app_v.model.base_data.name)
self.cellWidget(idx, 2).setText(app_v.model.base_data.latest_version)
self._set_col_description(self.item(idx, 3), app_v)
app_v.status = ApplicationViewStatus.READY
self.window.resize_and_center()
self.lock_async_data.release()
def get_selected_app(self) -> ApplicationView:
return self.window.apps[self.currentRow()] return self.window.apps[self.currentRow()]
def get_selected_app_icon(self): def get_selected_app_icon(self) -> QIcon:
return self.item(self.currentRow(), 0).icon() return self.item(self.currentRow(), 0).icon()
def _uninstall_app(self): def _uninstall_app(self):
selected_app = self.get_selected_app() selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys): locale_keys=self.window.locale_keys):
self.window.uninstall_app(selected_app['model']['ref']) self.window.uninstall_app(selected_app)
def _downgrade_app(self): def _downgrade_app(self):
selected_app = self.get_selected_app() selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'], if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']), body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys): locale_keys=self.window.locale_keys):
self.window.downgrade_app(selected_app) self.window.downgrade_app(selected_app)
@@ -130,96 +165,100 @@ class AppsTable(QTableWidget):
def _install_app(self): def _install_app(self):
self.window.install_app(self.get_selected_app()) self.window.install_app(self.get_selected_app())
def _load_icon(self, http_response): def _load_icon_and_cache(self, http_response):
icon_url = http_response.url().toString() icon_url = http_response.url().toString()
pixmap = QPixmap()
pixmap.loadFromData(http_response.readAll()) icon_data = self.icon_cache.get(icon_url)
icon = QIcon(pixmap) icon_was_cached = True
self.icon_cache[icon_url] = icon
if not icon_data:
icon_was_cached = False
pixmap = QPixmap()
icon_bytes = http_response.readAll()
pixmap.loadFromData(icon_bytes)
icon = QIcon(pixmap)
icon_data = {'icon': icon, 'bytes': icon_bytes}
self.icon_cache.add(icon_url, icon_data)
for idx, app in enumerate(self.window.apps): for idx, app in enumerate(self.window.apps):
if app['model']['icon'] == icon_url: if app.model.base_data.icon_url == icon_url:
self.item(idx, 0).setIcon(icon) col_name = self.item(idx, 0)
self.window.resize_and_center() col_name.setIcon(icon_data['icon'])
break
def update_apps(self, apps: List[dict]): if self.disk_cache and app.model.supports_disk_cache():
if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
self.window.manager.cache_to_disk(app=app.model, icon_bytes=icon_data['bytes'], only_icon=True)
def update_apps(self, app_views: List[ApplicationView]):
self.setEnabled(True) self.setEnabled(True)
self.setRowCount(len(apps) if apps else 0) self.setRowCount(len(app_views) if app_views else 0)
if apps:
for idx, app in enumerate(apps):
tooltip = util.strip_html(app['model']['description']) if app['model']['description'] else None
col_name = QTableWidgetItem()
col_name.setText(app['model']['name'])
col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_name.setToolTip(tooltip)
if not app['model']['icon']:
col_name.setIcon(self.icon_flathub)
else:
cached_icon = self.icon_cache.get(app['model']['icon'])
if cached_icon:
col_name.setIcon(cached_icon)
else:
col_name.setIcon(self.icon_flathub)
self.network_man.get(QNetworkRequest(QUrl(app['model']['icon'])))
if app_views:
for idx, app_v in enumerate(app_views):
col_name = self._gen_col_name(app_v)
self.setItem(idx, 0, col_name) self.setItem(idx, 0, col_name)
col_version = QTableWidgetItem() col_version = QLabel(app_v.model.base_data.version)
col_version.setText(app['model']['version']) col_version.setAlignment(Qt.AlignCenter)
col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.setCellWidget(idx, 1, col_version)
col_version.setToolTip(tooltip)
self.setItem(idx, 1, col_version)
col_release = QTableWidgetItem() col_release = QLabel(app_v.get_async_attr('latest_version'))
col_release.setText(app['model']['latest_version']) col_release.setAlignment(Qt.AlignCenter)
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.setCellWidget(idx, 2, col_release)
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_release.setStyleSheet("color: orange")
col_branch = QTableWidgetItem() col_description = QTableWidgetItem()
col_branch.setText(app['model']['branch']) col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self._set_col_description(col_description, app_v)
col_branch.setToolTip(tooltip)
self.setItem(idx, 3, col_branch)
col_arch = QTableWidgetItem() self.setItem(idx, 3, col_description)
col_arch.setText(app['model']['arch'])
col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_arch.setToolTip(tooltip)
self.setItem(idx, 4, col_arch)
col_id = QTableWidgetItem() col_type = QLabel(app_v.model.get_type())
col_id.setText(app['model']['id']) col_type.setAlignment(Qt.AlignCenter)
col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.setCellWidget(idx, 4, col_type)
col_id.setToolTip(tooltip)
self.setItem(idx, 5, col_id)
col_origin = QTableWidgetItem()
col_origin.setText(app['model']['origin'])
col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_origin.setToolTip(tooltip)
self.setItem(idx, 6, col_origin)
col_installed = QLabel() col_installed = QLabel()
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross'))))) col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app_v.model.installed else 'red_cross')))))
col_installed.setToolTip(tooltip)
col_installed.setAlignment(Qt.AlignCenter) col_installed.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 7, col_installed) self.setCellWidget(idx, 5, col_installed)
col_update = UpdateToggleButton(app, self.window, self.window.locale_keys, app['model']['update']) if app['model']['update'] else None col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update) if app_v.model.update else None
self.setCellWidget(idx, 8, col_update) self.setCellWidget(idx, 6, col_update)
def _gen_col_name(self, app_v: ApplicationView):
col = QTableWidgetItem()
col.setText(app_v.model.base_data.name if app_v.model.base_data.name else '...')
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if self.disk_cache and app_v.model.supports_disk_cache() and os.path.exists(app_v.model.get_disk_icon_path()):
with open(app_v.model.get_disk_icon_path(), 'rb') as f:
icon_bytes = f.read()
pixmap = QPixmap()
pixmap.loadFromData(icon_bytes)
icon = QIcon(pixmap)
self.icon_cache.add_non_existing(app_v.model.base_data.icon_url, {'icon': icon, 'bytes': icon_bytes})
elif not app_v.model.base_data.icon_url:
icon = QIcon(app_v.model.get_default_icon_path())
else:
icon_data = self.icon_cache.get(app_v.model.base_data.icon_url)
icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path())
col.setIcon(icon)
return col
def _set_col_description(self, col: QTableWidgetItem, app_v: ApplicationView):
desc = app_v.get_async_attr('description', strip_html=True)
if desc:
col.setText(desc[0:25] + '...')
if app_v.model.status == ApplicationStatus.READY:
col.setToolTip(desc)
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
header_horizontal = self.horizontalHeader() header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()): for i in range(self.columnCount()):
header_horizontal.setSectionResizeMode(i, policy) header_horizontal.setSectionResizeMode(i, policy)

View File

@@ -8,10 +8,10 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem
class HistoryDialog(QDialog): class HistoryDialog(QDialog):
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict): def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
super(HistoryDialog, self).__init__() super(HistoryDialog, self).__init__()
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name'])) self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name))
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
layout = QVBoxLayout() layout = QVBoxLayout()
@@ -23,13 +23,13 @@ class HistoryDialog(QDialog):
table_history.verticalHeader().setVisible(False) table_history.verticalHeader().setVisible(False)
table_history.setAlternatingRowColors(True) table_history.setAlternatingRowColors(True)
table_history.setColumnCount(len(app['commits'][0])) table_history.setColumnCount(len(app['history'][0]))
table_history.setRowCount(len(app['commits'])) table_history.setRowCount(len(app['history']))
table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['commits'][0].keys())]) table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['history'][0].keys())])
for row, commit in enumerate(app['commits']): for row, commit in enumerate(app['history']):
current_app_commit = app['model']['commit'] == commit['commit'] current_app_commit = app['model'].commit == commit['commit']
for col, key in enumerate(sorted(commit.keys())): for col, key in enumerate(sorted(commit.keys())):
item = QTableWidgetItem() item = QTableWidgetItem()

View File

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

View File

@@ -1,37 +1,62 @@
import time import time
from datetime import datetime, timedelta
from typing import List
import requests
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from fpakman.core import flatpak from fpakman.core.controller import ApplicationManager
from fpakman.core.controller import FlatpakManager from fpakman.core.exception import NoInternetException
from fpakman.core.model import ApplicationStatus
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView
class UpdateSelectedApps(QThread): class UpdateSelectedApps(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self): def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
super(UpdateSelectedApps, self).__init__() super(UpdateSelectedApps, self).__init__()
self.refs_to_update = [] self.apps_to_update = apps_to_update
self.manager = manager
def run(self): def run(self):
for app_ref in self.refs_to_update: error = False
for output in flatpak.update_and_stream(app_ref):
for app in self.apps_to_update:
subproc = self.manager.update_and_stream(app.model)
self.signal_output.emit(' '.join(subproc.args) + '\n')
for output in subproc.stdout:
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
self.signal_finished.emit() for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
self.signal_output.emit('\n')
if error:
break
self.signal_finished.emit(not error)
class RefreshApps(QThread): class RefreshApps(QThread):
signal = pyqtSignal(list) signal = pyqtSignal(list)
def __init__(self, manager: FlatpakManager): def __init__(self, manager: ApplicationManager):
super(RefreshApps, self).__init__() super(RefreshApps, self).__init__()
self.manager = manager self.manager = manager
@@ -43,85 +68,110 @@ class UninstallApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal()
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self): def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
super(UninstallApp, self).__init__() super(UninstallApp, self).__init__()
self.app_ref = None self.app = app
self.manager = manager
self.icon_cache = icon_cache
def run(self): def run(self):
if self.app_ref: if self.app:
for output in flatpak.uninstall_and_stream(self.app_ref): subproc = self.manager.uninstall_and_stream(self.app.model)
self.signal_output.emit(' '.join(subproc.args) + '\n')
for output in subproc.stdout:
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
error = False
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
if not error:
self.icon_cache.delete(self.app.model.base_data.icon_url)
self.manager.clean_cache_for(self.app.model)
self.signal_finished.emit() self.signal_finished.emit()
class DowngradeApp(QThread): class DowngradeApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self, manager: FlatpakManager, locale_keys: dict): def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None):
super(DowngradeApp, self).__init__() super(DowngradeApp, self).__init__()
self.manager = manager self.manager = manager
self.app = None self.app = app
self.root_password = None
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.root_password = None
def run(self): def run(self):
if self.app: if self.app:
stream = self.manager.downgrade_app(self.app['model'], self.root_password) success = True
try:
stream = self.manager.downgrade_app(self.app.model, self.root_password)
if stream is None: if stream is None:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body']) body=self.locale_keys['popup.downgrade.impossible.body'])
else: else:
for output in self.manager.downgrade_app(self.app['model'], self.root_password): for output in stream:
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
except (requests.exceptions.ConnectionError, NoInternetException):
self.app = None success = False
self.root_password = None self.signal_output.emit(self.locale_keys['internet.required'])
self.signal_finished.emit() finally:
self.app = None
self.root_password = None
self.signal_finished.emit(success)
class GetAppInfo(QThread): class GetAppInfo(QThread):
signal_finished = pyqtSignal(dict) signal_finished = pyqtSignal(dict)
def __init__(self): def __init__(self, manager: ApplicationManager, app: ApplicationView = None):
super(GetAppInfo, self).__init__() super(GetAppInfo, self).__init__()
self.app = None self.app = app
self.manager = manager
def run(self): def run(self):
if self.app: if self.app:
app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch']) self.signal_finished.emit(self.manager.get_info(self.app.model))
app_info['name'] = self.app['model']['name']
app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app'
app_info['description'] = self.app['model']['description']
self.signal_finished.emit(app_info)
self.app = None self.app = None
class GetAppHistory(QThread): class GetAppHistory(QThread):
signal_finished = pyqtSignal(dict) signal_finished = pyqtSignal(dict)
def __init__(self): def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None):
super(GetAppHistory, self).__init__() super(GetAppHistory, self).__init__()
self.app = None self.app = app
self.manager = manager
self.locale_keys = locale_keys
def run(self): def run(self):
if self.app: if self.app:
commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin']) try:
self.signal_finished.emit({'model': self.app['model'], 'commits': commits}) res = {'model': self.app.model, 'history': self.manager.get_history(self.app.model)}
self.app = None self.signal_finished.emit(res)
except (requests.exceptions.ConnectionError, NoInternetException):
self.signal_finished.emit({'error': self.locale_keys['internet.required']})
finally:
self.app = None
class SearchApps(QThread): class SearchApps(QThread):
signal_finished = pyqtSignal(list) signal_finished = pyqtSignal(list)
def __init__(self, manager: FlatpakManager): def __init__(self, manager: ApplicationManager):
super(SearchApps, self).__init__() super(SearchApps, self).__init__()
self.word = None self.word = None
self.manager = manager self.manager = manager
@@ -138,23 +188,46 @@ class SearchApps(QThread):
class InstallApp(QThread): class InstallApp(QThread):
signal_finished = pyqtSignal() signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self): def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, app: ApplicationView = None):
super(InstallApp, self).__init__() super(InstallApp, self).__init__()
self.app = None self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.disk_cache = disk_cache
def run(self): def run(self):
if self.app: if self.app:
for output in flatpak.install_and_stream(self.app['model']['id'], self.app['model']['origin']): subproc = self.manager.install_and_stream(self.app.model)
self.signal_output.emit(' '.join(subproc.args) + '\n')
for output in subproc.stdout:
line = output.decode().strip() line = output.decode().strip()
if line: if line:
self.signal_output.emit(line) self.signal_output.emit(line)
self.app = None error = False
self.signal_finished.emit()
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
if not error and self.disk_cache:
self.app.model.installed = True
if self.app.model.supports_disk_cache():
icon_data = self.icon_cache.get(self.app.model.base_data.icon_url)
self.manager.cache_to_disk(app=self.app.model,
icon_bytes=icon_data.get('bytes') if icon_data else None,
only_icon=False)
self.app = None
self.signal_finished.emit(not error)
class AnimateProgress(QThread): class AnimateProgress(QThread):
@@ -184,3 +257,38 @@ class AnimateProgress(QThread):
time.sleep(0.05) time.sleep(0.05)
self.progress_value = 0 self.progress_value = 0
class VerifyModels(QThread):
signal_updates = pyqtSignal()
def __init__(self, apps: List[ApplicationView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
def run(self):
if self.apps:
stop_at = datetime.utcnow() + timedelta(seconds=30)
last_ready = 0
while True:
current_ready = 0
for app in self.apps:
current_ready += 1 if app.model.status == ApplicationStatus.READY else 0
if current_ready > last_ready:
last_ready = current_ready
self.signal_updates.emit()
if current_ready == len(self.apps):
self.signal_updates.emit()
break
if stop_at <= datetime.utcnow():
break
self.apps = None

View File

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

View File

@@ -8,24 +8,25 @@ from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar
from fpakman.core import resource, flatpak from fpakman.core import resource
from fpakman.core.controller import FlatpakManager from fpakman.core.controller import ApplicationManager
from fpakman.view.qt import dialog from fpakman.core.model import Application
from fpakman.util.cache import Cache
from fpakman.view.qt.apps_table import AppsTable from fpakman.view.qt.apps_table import AppsTable
from fpakman.view.qt.history import HistoryDialog from fpakman.view.qt.history import HistoryDialog
from fpakman.view.qt.info import InfoDialog from fpakman.view.qt.info import InfoDialog
from fpakman.view.qt.root import is_root, ask_root_password from fpakman.view.qt.root import is_root, ask_root_password
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels
from fpakman.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500' DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget): class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400 __BASE_HEIGHT__ = 400
def __init__(self, locale_keys: dict, manager: FlatpakManager, tray_icon=None): def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, tray_icon=None):
super(ManageWindow, self).__init__() super(ManageWindow, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager
@@ -34,9 +35,10 @@ class ManageWindow(QWidget):
self.working = False # restrict the number of threaded actions self.working = False # restrict the number of threaded actions
self.apps = [] self.apps = []
self.label_flatpak = None self.label_flatpak = None
self.icon_cache = icon_cache
self.disk_cache = disk_cache
self.icon_flathub = QIcon(resource.get_path('img/logo.svg')) self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
self._check_flatpak_installed()
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
self.setWindowTitle(locale_keys['manage_window.title']) self.setWindowTitle(locale_keys['manage_window.title'])
self.setWindowIcon(self.icon_flathub) self.setWindowIcon(self.icon_flathub)
@@ -47,19 +49,16 @@ class ManageWindow(QWidget):
self.toolbar_search = QToolBar() self.toolbar_search = QToolBar()
self.toolbar_search.setStyleSheet("spacing: 0px;") self.toolbar_search.setStyleSheet("spacing: 0px;")
self.toolbar_search.setContentsMargins(0, 0, 0, 0) self.toolbar_search.setContentsMargins(0, 0, 0, 0)
self.toolbar_search.addWidget(self._new_spacer())
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_search.addWidget(spacer)
label_pre_search = QLabel() label_pre_search = QLabel()
label_pre_search.setStyleSheet("background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;") label_pre_search.setStyleSheet(
"background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
self.toolbar_search.addWidget(label_pre_search) self.toolbar_search.addWidget(label_pre_search)
self.input_search = QLineEdit() self.input_search = QLineEdit()
self.input_search.setFrame(False) self.input_search.setFrame(False)
self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder']+"...") self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder'] + "...")
self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip']) self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip'])
self.input_search.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}") self.input_search.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}")
self.input_search.returnPressed.connect(self.search) self.input_search.returnPressed.connect(self.search)
@@ -70,9 +69,7 @@ class ManageWindow(QWidget):
label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;") label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;")
self.toolbar_search.addWidget(label_pos_search) self.toolbar_search.addWidget(label_pos_search)
spacer = QWidget() self.toolbar_search.addWidget(self._new_spacer())
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_search.addWidget(spacer)
self.layout.addWidget(self.toolbar_search) self.layout.addWidget(self.toolbar_search)
toolbar = QToolBar() toolbar = QToolBar()
@@ -83,23 +80,19 @@ class ManageWindow(QWidget):
self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps) self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps)
toolbar.addWidget(self.checkbox_only_apps) toolbar.addWidget(self.checkbox_only_apps)
spacer = QWidget() toolbar.addWidget(self._new_spacer())
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
toolbar.addWidget(spacer)
self.label_status = QLabel() self.label_status = QLabel()
self.label_status.setText('') self.label_status.setText('')
self.label_status.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) self.label_status.setStyleSheet("font-weight: bold")
toolbar.addWidget(self.label_status) toolbar.addWidget(self.label_status)
spacer = QWidget() toolbar.addWidget(self._new_spacer())
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
toolbar.addWidget(spacer)
self.bt_refresh = QToolButton() self.bt_refresh = QToolButton()
self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip']) self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip'])
self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
self.bt_refresh.clicked.connect(lambda: self.refresh(clear_output=True)) self.bt_refresh.clicked.connect(lambda: self.refresh(keep_console=False))
toolbar.addWidget(self.bt_refresh) toolbar.addWidget(self.bt_refresh)
self.bt_upgrade = QToolButton() self.bt_upgrade = QToolButton()
@@ -111,11 +104,22 @@ class ManageWindow(QWidget):
self.layout.addWidget(toolbar) self.layout.addWidget(toolbar)
self.table_apps = AppsTable(self) self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache)
self.table_apps.change_headers_policy() self.table_apps.change_headers_policy()
self.layout.addWidget(self.table_apps) self.layout.addWidget(self.table_apps)
toolbar_console = QToolBar()
self.checkbox_console = QCheckBox()
self.checkbox_console.setText(self.locale_keys['manage_window.checkbox.show_details'])
self.checkbox_console.stateChanged.connect(self._handle_console)
self.checkbox_console.setVisible(False)
self.ref_checkbox_console = toolbar_console.addWidget(self.checkbox_console)
toolbar_console.addWidget(self._new_spacer())
self.layout.addWidget(toolbar_console)
self.textarea_output = QPlainTextEdit(self) self.textarea_output = QPlainTextEdit(self)
self.textarea_output.resize(self.table_apps.size()) self.textarea_output.resize(self.table_apps.size())
self.textarea_output.setStyleSheet("background: black; color: white;") self.textarea_output.setStyleSheet("background: black; color: white;")
@@ -123,14 +127,14 @@ class ManageWindow(QWidget):
self.textarea_output.setVisible(False) self.textarea_output.setVisible(False)
self.textarea_output.setReadOnly(True) self.textarea_output.setReadOnly(True)
self.thread_update = UpdateSelectedApps() self.thread_update = UpdateSelectedApps(self.manager)
self.thread_update.signal_output.connect(self._update_action_output) self.thread_update.signal_output.connect(self._update_action_output)
self.thread_update.signal_finished.connect(self._finish_update_selected) self.thread_update.signal_finished.connect(self._finish_update_selected)
self.thread_refresh = RefreshApps(self.manager) self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh) self.thread_refresh.signal.connect(self._finish_refresh)
self.thread_uninstall = UninstallApp() self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
self.thread_uninstall.signal_output.connect(self._update_action_output) self.thread_uninstall.signal_output.connect(self._update_action_output)
self.thread_uninstall.signal_finished.connect(self._finish_uninstall) self.thread_uninstall.signal_finished.connect(self._finish_uninstall)
@@ -138,46 +142,50 @@ class ManageWindow(QWidget):
self.thread_downgrade.signal_output.connect(self._update_action_output) self.thread_downgrade.signal_output.connect(self._update_action_output)
self.thread_downgrade.signal_finished.connect(self._finish_downgrade) self.thread_downgrade.signal_finished.connect(self._finish_downgrade)
self.thread_get_info = GetAppInfo() self.thread_get_info = GetAppInfo(self.manager)
self.thread_get_info.signal_finished.connect(self._finish_get_info) self.thread_get_info.signal_finished.connect(self._finish_get_info)
self.thread_get_history = GetAppHistory() self.thread_get_history = GetAppHistory(self.manager, self.locale_keys)
self.thread_get_history.signal_finished.connect(self._finish_get_history) self.thread_get_history.signal_finished.connect(self._finish_get_history)
self.thread_search = SearchApps(self.manager) self.thread_search = SearchApps(self.manager)
self.thread_search.signal_finished.connect(self._finish_search) self.thread_search.signal_finished.connect(self._finish_search)
self.thread_install = InstallApp() self.thread_install = InstallApp(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache)
self.thread_install.signal_output.connect(self._update_action_output) self.thread_install.signal_output.connect(self._update_action_output)
self.thread_install.signal_finished.connect(self._finish_install) self.thread_install.signal_finished.connect(self._finish_install)
self.thread_animate_progress = AnimateProgress() self.thread_animate_progress = AnimateProgress()
self.thread_animate_progress.signal_change.connect(self._update_progress) self.thread_animate_progress.signal_change.connect(self._update_progress)
self.thread_verify_models = VerifyModels()
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)
self.toolbar_bottom = QToolBar() self.toolbar_bottom = QToolBar()
self.label_updates = QLabel('') self.label_updates = QLabel('')
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
self.toolbar_bottom.addWidget(self.label_updates) self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
spacer = QWidget() self.toolbar_bottom.addWidget(self._new_spacer())
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_bottom.addWidget(spacer)
self.progress_bar = QProgressBar() self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(False) self.progress_bar.setTextVisible(False)
self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar) self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar)
spacer = QWidget() self.toolbar_bottom.addWidget(self._new_spacer())
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_bottom.addWidget(spacer)
self.label_flatpak = QLabel(self._get_flatpak_label())
self.toolbar_bottom.addWidget(self.label_flatpak)
self.layout.addWidget(self.toolbar_bottom) self.layout.addWidget(self.toolbar_bottom)
self.centralize() 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)
return spacer
def changeEvent(self, e: QEvent): def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent): if isinstance(e, QWindowStateChangeEvent):
@@ -189,20 +197,7 @@ class ManageWindow(QWidget):
if self.tray_icon: if self.tray_icon:
event.ignore() event.ignore()
self.hide() self.hide()
self._handle_console_option(False)
def _check_flatpak_installed(self):
if not flatpak.is_installed():
dialog.show_error(title=self.locale_keys['popup.flatpak_not_installed.title'],
body=self.locale_keys['popup.flatpak_not_installed.msg'] + '...',
icon=self.icon_flathub)
exit(1)
if self.label_flatpak:
self.label_flatpak.setText(self._get_flatpak_label())
def _get_flatpak_label(self):
return 'flatpak: ' + flatpak.get_version()
def _acquire_lock(self): def _acquire_lock(self):
@@ -223,61 +218,70 @@ class ManageWindow(QWidget):
self.thread_lock.release() self.thread_lock.release()
def _hide_output(self): def _handle_console(self, checked: bool):
self.textarea_output.clear()
if checked:
self.textarea_output.show()
else:
self.textarea_output.hide()
def _handle_console_option(self, enable: bool):
if enable:
self.textarea_output.clear()
self.ref_checkbox_console.setVisible(enable)
self.checkbox_console.setChecked(False)
self.textarea_output.hide() self.textarea_output.hide()
def _show_output(self): def refresh(self, keep_console: bool = True):
self.textarea_output.clear()
self.textarea_output.show()
def refresh(self, clear_output: bool = True):
if self._acquire_lock(): if self._acquire_lock():
self._check_flatpak_installed() self.input_search.clear()
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
if clear_output: if not keep_console:
self._hide_output() self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
self.thread_refresh.start() self.thread_refresh.start()
def _finish_refresh(self, apps: List[dict]): def _finish_refresh(self, apps: List[Application]):
self.update_apps(apps) self.update_apps(apps)
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
def uninstall_app(self, app_ref: str): def uninstall_app(self, app: ApplicationView):
self._check_flatpak_installed()
if self._acquire_lock(): if self._acquire_lock():
self.textarea_output.clear() self._handle_console_option(True)
self.textarea_output.setVisible(True) self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.uninstalling'], app.model.base_data.name))
self._begin_action(self.locale_keys['manage_window.status.uninstalling'])
self.thread_uninstall.app_ref = app_ref self.thread_uninstall.app = app
self.thread_uninstall.start() self.thread_uninstall.start()
def _finish_uninstall(self): def _finish_uninstall(self):
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
self.refresh(clear_output=False) self.refresh()
def _finish_downgrade(self): def _finish_downgrade(self, success: bool):
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
self.refresh(clear_output=False)
if success:
self.refresh()
else:
self.checkbox_console.setChecked(True)
def filter_only_apps(self, only_apps: int): def filter_only_apps(self, only_apps: int):
if self.apps: if self.apps:
show_only_apps = True if only_apps == 2 else False show_only_apps = True if only_apps == 2 else False
for idx, app in enumerate(self.apps): for idx, app_v in enumerate(self.apps):
hidden = show_only_apps and app['model']['runtime'] hidden = show_only_apps and app_v.model.is_library()
self.table_apps.setRowHidden(idx, hidden) self.table_apps.setRowHidden(idx, hidden)
app['visible'] = not hidden app_v.visible = not hidden
self.change_update_state() self.change_update_state()
self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy(QHeaderView.Stretch)
@@ -288,31 +292,32 @@ class ManageWindow(QWidget):
enable_bt_update = False enable_bt_update = False
app_updates, runtime_updates = 0, 0 app_updates, library_updates = 0, 0
for app in self.apps: for app_v in self.apps:
if app['model']['update']: if app_v.model.update:
if app['model']['runtime']: if app_v.model.runtime:
runtime_updates += 1 library_updates += 1
else: else:
app_updates += 1 app_updates += 1
total_updates = app_updates + runtime_updates total_updates = app_updates + library_updates
if total_updates > 0: if total_updates > 0:
self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates)) self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates))
self.label_updates.setToolTip('{} {} | {} runtimes'.format(app_updates, self.label_updates.setToolTip('{} {} | {} {}'.format(app_updates,
self.locale_keys['manage_window.checkbox.only_apps'].lower(), self.locale_keys['manage_window.checkbox.only_apps'].lower(),
runtime_updates)) library_updates,
self.locale_keys['others'].lower()))
else: else:
self.label_updates.setText('') self.label_updates.setText('')
for app in self.apps: for app_v in self.apps:
if app['visible'] and app['update_checked']: if app_v.visible and app_v.update_checked:
enable_bt_update = True enable_bt_update = True
break break
self.bt_upgrade.setEnabled(enable_bt_update) self.bt_upgrade.setEnabled(enable_bt_update)
self.tray_icon.notify_updates([app['model'] for app in self.apps if app['model']['update']]) self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
def centralize(self): def centralize(self):
geo = self.frameGeometry() geo = self.frameGeometry()
@@ -321,20 +326,17 @@ class ManageWindow(QWidget):
geo.moveCenter(center_point) geo.moveCenter(center_point)
self.move(geo.topLeft()) self.move(geo.topLeft())
def update_apps(self, apps: List[dict]): def update_apps(self, apps: List[Application]):
self._check_flatpak_installed()
self.apps = [] self.apps = []
napps = 0 # number of apps (not runtimes) napps = 0 # number of apps (not libraries)
if apps: if apps:
for app in apps: for app in apps:
app_model = {'model': app, app_model = ApplicationView(model=app,
'update_checked': app['update'], visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked())
'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()}
napps += 1 if not app['runtime'] else 0 napps += 1 if not app.is_library() else 0
self.apps.append(app_model) self.apps.append(app_model)
if napps == 0: if napps == 0:
@@ -349,6 +351,9 @@ class ManageWindow(QWidget):
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0) self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
self.resize_and_center() self.resize_and_center()
self.thread_verify_models.apps = self.apps
self.thread_verify_models.start()
def resize_and_center(self): def resize_and_center(self):
new_width = reduce(operator.add, [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.resize(new_width, self.height())
@@ -359,55 +364,59 @@ class ManageWindow(QWidget):
if self._acquire_lock(): if self._acquire_lock():
if self.apps: if self.apps:
to_update = [pak['model']['ref'] for pak in self.apps if pak['visible'] and pak['update_checked']] to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked]
if to_update: if to_update:
self.textarea_output.clear() self._handle_console_option(True)
self.textarea_output.setVisible(True)
self._begin_action(self.locale_keys['manage_window.status.upgrading']) self._begin_action(self.locale_keys['manage_window.status.upgrading'])
self.thread_update.refs_to_update = to_update self.thread_update.apps_to_update = to_update
self.thread_update.start() self.thread_update.start()
def _finish_update_selected(self): def _finish_update_selected(self, success: bool):
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
self.refresh(clear_output=False)
if success:
self.refresh()
else:
self.bt_upgrade.setEnabled(True)
self.checkbox_console.setChecked(True)
def _update_action_output(self, output: str): def _update_action_output(self, output: str):
self.textarea_output.appendPlainText(output) self.textarea_output.appendPlainText(output)
def _begin_action(self, action_label: str): def _begin_action(self, action_label: str, keep_search: bool = False):
self.ref_label_updates.setVisible(False)
self.thread_animate_progress.stop = False self.thread_animate_progress.stop = False
self.thread_animate_progress.start() self.thread_animate_progress.start()
self.ref_progress_bar.setVisible(True) self.ref_progress_bar.setVisible(True)
self.progress_bar.setValue(50)
self.label_status.setText(action_label + "...") self.label_status.setText(action_label + "...")
self.toolbar_search.setVisible(False)
self.bt_upgrade.setEnabled(False) self.bt_upgrade.setEnabled(False)
self.bt_refresh.setEnabled(False) self.bt_refresh.setEnabled(False)
self.checkbox_only_apps.setEnabled(False) self.checkbox_only_apps.setEnabled(False)
self.table_apps.setEnabled(False) self.table_apps.setEnabled(False)
def finish_action(self, clear_search: bool = True): if keep_search:
self.thread_animate_progress.stop = True self.toolbar_search.setEnabled(False)
else:
self.toolbar_search.setVisible(False)
def finish_action(self):
self.ref_progress_bar.setVisible(False) self.ref_progress_bar.setVisible(False)
self.ref_label_updates.setVisible(True)
self.thread_animate_progress.stop = True
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
self.bt_refresh.setEnabled(True) self.bt_refresh.setEnabled(True)
self.toolbar_search.setVisible(True)
self.checkbox_only_apps.setEnabled(True) self.checkbox_only_apps.setEnabled(True)
self.table_apps.setEnabled(True) self.table_apps.setEnabled(True)
self.input_search.setEnabled(True) self.input_search.setEnabled(True)
self.label_status.setText('') self.label_status.setText('')
self.toolbar_search.setVisible(True)
self.toolbar_search.setEnabled(True)
if clear_search: def downgrade_app(self, app: ApplicationView):
self.input_search.setText('')
def downgrade_app(self, app: dict):
self._check_flatpak_installed()
if self._acquire_lock(): if self._acquire_lock():
pwd = None pwd = None
@@ -419,9 +428,8 @@ class ManageWindow(QWidget):
self._release_lock() self._release_lock()
return return
self.textarea_output.clear() self._handle_console_option(True)
self.textarea_output.setVisible(True) self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name))
self._begin_action(self.locale_keys['manage_window.status.downgrading'])
self.thread_downgrade.app = app self.thread_downgrade.app = app
self.thread_downgrade.root_password = pwd self.thread_downgrade.root_password = pwd
@@ -430,8 +438,7 @@ class ManageWindow(QWidget):
def get_app_info(self, app: dict): def get_app_info(self, app: dict):
if self._acquire_lock(): if self._acquire_lock():
self.textarea_output.clear() self._handle_console_option(False)
self.textarea_output.setVisible(False)
self._begin_action(self.locale_keys['manage_window.status.info']) self._begin_action(self.locale_keys['manage_window.status.info'])
self.thread_get_info.app = app self.thread_get_info.app = app
@@ -439,8 +446,7 @@ class ManageWindow(QWidget):
def get_app_history(self, app: dict): def get_app_history(self, app: dict):
if self._acquire_lock(): if self._acquire_lock():
self.textarea_output.clear() self._handle_console_option(False)
self.textarea_output.setVisible(False)
self._begin_action(self.locale_keys['manage_window.status.history']) self._begin_action(self.locale_keys['manage_window.status.history'])
self.thread_get_history.app = app self.thread_get_history.app = app
@@ -457,42 +463,47 @@ class ManageWindow(QWidget):
self._release_lock() self._release_lock()
self.finish_action() self.finish_action()
self.change_update_state() self.change_update_state()
dialog_history = HistoryDialog(app, self.table_apps.get_selected_app_icon(), self.locale_keys)
dialog_history.exec_() if app.get('error'):
self._handle_console_option(True)
self.textarea_output.appendPlainText(app['error'])
self.checkbox_console.setChecked(True)
else:
dialog_history = HistoryDialog(app, self.table_apps.get_selected_app_icon(), self.locale_keys)
dialog_history.exec_()
def search(self): def search(self):
word = self.input_search.text().strip() word = self.input_search.text().strip()
if word and self._acquire_lock(): if word and self._acquire_lock():
self.textarea_output.clear() self._handle_console_option(False)
self.textarea_output.setVisible(False) self._begin_action(self.locale_keys['manage_window.status.searching'], keep_search=True)
self._begin_action(self.locale_keys['manage_window.status.searching'])
self.thread_search.word = word self.thread_search.word = word
self.thread_search.start() self.thread_search.start()
def _finish_search(self, apps_found: List[dict]): def _finish_search(self, apps_found: List[Application]):
self._release_lock() self._release_lock()
self.finish_action(clear_search=False) self.finish_action()
self.update_apps(apps_found) self.update_apps(apps_found)
def install_app(self, app: dict): def install_app(self, app: ApplicationView):
self._check_flatpak_installed()
if self._acquire_lock(): if self._acquire_lock():
self._begin_action(self.locale_keys['manage_window.status.installing']) self._handle_console_option(True)
self._show_output() self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.installing'], app.model.base_data.name))
self.thread_install.app = app self.thread_install.app = app
self.thread_install.start() self.thread_install.start()
def _finish_install(self): def _finish_install(self, success: bool):
self.input_search.setText('') self.input_search.setText('')
self.finish_action() self.finish_action()
self._release_lock() self._release_lock()
self.refresh(clear_output=False)
if success:
self.refresh()
else:
self.checkbox_console.setChecked(True)
def _update_progress(self, value: int): def _update_progress(self, value: int):
self.progress_bar.setValue(value) self.progress_bar.setValue(value)