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/).
## [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
### Features
- 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_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60.
- **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale.
- **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for application data retrieved from the origin API. Default: 3600 (1 hour).
- **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for cached API data. Default: 3600 (1 hour).
- **FPAKMAN_ICON_EXPIRATION**: define a custom expiration time in SECONDS for cached icons. Default: 300 (5 minutes).
- **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

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 colorama import Fore
from fpakman import __version__
from fpakman import __version__, __app_name__
from fpakman.core import resource
from fpakman.core import util
from fpakman.core.controller import FlatpakManager
from fpakman.core.disk import DiskCacheLoaderFactory
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
app_name = 'fpakman'
def log_msg(msg: str, color: int = None):
if color is None:
print('[{}] {}'.format(app_name, msg))
print('[{}] {}'.format(__app_name__, msg))
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('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached application expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached API data expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('FPAKMAN_ICON_EXPIRATION', 60 * 5)), type=int, help='cached icons expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s')
parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s')
parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enable/disable system notifications for new updates. Default: %(default)s')
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()
if args.cache_exp <= 0:
if args.cache_exp < 0:
log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if args.icon_exp < 0:
log_msg("'icon-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW)
if not args.locale.strip():
log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED)
exit(1)
@@ -47,16 +54,30 @@ if args.update_notification == 0:
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.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,
manager=manager,
check_interval=args.check_interval,
icon_cache=icon_cache,
disk_cache=args.disk_cache,
update_notification=bool(args.update_notification))
trayIcon.load_database()
trayIcon.show()
CacheCleaner(caches).start()
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 typing import List
import requests
from fpakman.core import flatpak
__FLATHUB_URL__ = 'https://flathub.org'
__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1'
from fpakman.core import flatpak, disk
from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory
from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus, Application
from fpakman.util.cache import Cache
class FlatpakManager:
class ApplicationManager(ABC):
def __init__(self, cache_expire: int = 60 * 60):
self.cache_apps = {}
self.cache_expire = cache_expire
@abstractmethod
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[Application]:
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.lock_db_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:
res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30)
model = FlatpakApplication(arch=app.get('arch'),
branch=app.get('branch'),
origin=app.get('origin'),
runtime=app.get('runtime'),
ref=app.get('ref'),
commit=app.get('commit'),
base_data=ApplicationData(id=app.get('id'),
name=app.get('name'),
version=app.get('version'),
latest_version=app.get('latest_version')))
model.installed = installed
if res.status_code == 200:
for app in res.json():
self.cache_apps[app['flatpakAppId']] = app
finally:
self.lock_db_read.release()
api_data = self.api_cache.get(app['id'])
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:
res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30)
if not api_data or expired_data:
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:
app['latest_version'] = api_data.get('currentReleaseVersion')
app['icon'] = api_data.get('iconMobileUrl')
model.fill_cached_data(api_data)
for attr in ('name', 'description'):
if not app.get(attr):
app[attr] = api_data.get(attr)
return model
if app['icon'].startswith('/'):
app['icon'] = __FLATHUB_URL__ + app['icon']
def search(self, word: str) -> List[dict]:
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
res = []
apps_found = flatpak.search(word)
if apps_found:
already_read = set()
installed_apps = self.read_installed()
installed_apps = self.read_installed(disk_loader=disk_loader, keep_workers=True)
if installed_apps:
for app in apps_found:
for app_found in apps_found:
for installed_app in installed_apps:
if app['id'] == installed_app['id']:
if app_found['id'] == installed_app.base_data.id:
res.append(installed_app)
already_read.add(app['id'])
already_read.add(app_found['id'])
for app in apps_found:
if app['id'] not in already_read:
app['update'] = False
app['installed'] = False
self._fill_api_data(app)
res.append(app)
for app_found in apps_found:
if app_found['id'] not in already_read:
res.append(self._map_to_model(app_found, False, disk_loader))
disk_loader.stop = True
disk_loader.join()
self.async_data_loader.stop_current_workers()
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()
@@ -112,23 +151,166 @@ class FlatpakManager:
available_updates = flatpak.list_updates_as_str()
for app in installed:
self._fill_api_data(app)
app['update'] = app['id'] in available_updates
app['installed'] = True
models = []
return installed
for app in installed:
model = self._map_to_model(app, 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:
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'])
commit_idx = commits.index(app['commit'])
def downgrade_app(self, app: FlatpakApplication, root_password: str):
commits = flatpak.get_app_commits(app.ref, app.origin)
commit_idx = commits.index(app.commit)
# downgrade is not possible if the app current commit in the first one:
if commit_idx == len(commits) - 1:
return None
return flatpak.downgrade_and_stream(app['ref'], commits[commit_idx + 1], root_password)
return flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password)
def clean_cache_for(self, app: 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 fpakman.core import system
from fpakman.core.exception import NoInternetException
from fpakman.core.model import Application
BASE_CMD = 'flatpak'
@@ -72,7 +74,7 @@ def is_installed():
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
@@ -97,7 +99,7 @@ def update_and_stream(app_ref: str):
:param app_ref:
: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):
@@ -106,7 +108,7 @@ def uninstall_and_stream(app_ref: str):
:param app_ref:
: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():
@@ -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]:
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]:
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)
commits = []
@@ -210,4 +219,9 @@ def search(word: str) -> List[dict]:
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
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False) -> str:
res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, env={'LANG': 'en'})
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str:
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
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')):

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.installed=Installed
manage_window.columns.update=Upgrade ?
manage_window.apps_table.row.actions.info=Information
manage_window.apps_table.row.actions.history=History
@@ -25,8 +24,7 @@ manage_window.status.searching=Searching
manage_window.status.installing=Installing
manage_window.bt.refresh.tooltip=Reload the data about installed applications
manage_window.bt.upgrade.tooltip=Upgrade all selected applications
popup.flatpak_not_installed.title=Error
popup.flatpak_not_installed.msg=flatpak seems not to be installed. Exiting
manage_window.checkbox.show_details=Show details
popup.root.title=Requires root privileges
popup.root.password=Password
popup.root.bad_password.title=Error
@@ -42,7 +40,7 @@ tray.action.manage=Manage applications
tray.action.exit=Exit
tray.action.about=About
tray.action.refreshing=Refreshing
notification.new_updates={} updates
notification.new_updates=Updates
flatpak.info.arch=arch
flatpak.info.branch=branch
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
no=no
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.update=Actualizar ?
manage_window.columns.installed=Instalado
@@ -25,8 +25,7 @@ manage_window.status.searching=Buscando
manage_window.status.installing=Instalando
manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados
manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados
popup.flatpak_not_installed.title=Error
popup.flatpak_not_installed.msg=flatpak no parece estar instalado. Saliendo
manage_window.checkbox.show_details=Mostrar detalles
popup.root.title=Requiere privilegios de root
popup.root.password=Contraseña
popup.root.bad_password.title=Error
@@ -42,7 +41,7 @@ tray.action.manage=Administrar aplicativos
tray.action.exit=Salir
tray.action.about=Sobre
tray.action.refreshing=Recargando
notification.new_updates=Actualizaciones {}
notification.new_updates=Actualizaciones
flatpak.info.arch=arquitectura
flatpak.info.branch=rama
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í
no=no
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.update=Atualizar ?
manage_window.columns.installed=Instalado
@@ -25,8 +25,7 @@ manage_window.status.searching=Buscando
manage_window.status.installing=Instalando
manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados
manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados
popup.flatpak_not_installed.title=Erro
popup.flatpak_not_installed.msg=flatpak não parece estar instalado. Saindo
manage_window.checkbox.show_details=Mostrar detalhes
popup.root.title=Requer privilégios de root
popup.root.password=Senha
popup.root.bad_password.title=Erro
@@ -42,7 +41,7 @@ tray.action.manage=Gerenciar aplicativos
tray.action.exit=Sair
tray.action.about=Sobre
tray.action.refreshing=Recarregando
notification.new_updates=Atualizações {}
notification.new_updates=Atualizações
flatpak.info.arch=arquitetura
flatpak.info.branch=ramo
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
no=não
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.QtWidgets import QVBoxLayout, QDialog, QLabel
from fpakman import __version__
from fpakman import __version__, __app_name__
from fpakman.core import resource
PROJECT_URL = 'https://github.com/vinifmor/fpakman'
@@ -22,7 +22,7 @@ class AboutDialog(QDialog):
label_logo.setAlignment(Qt.AlignCenter)
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.setAlignment(Qt.AlignCenter)
layout.addWidget(label_name)

View File

@@ -1,20 +1,25 @@
import os
from threading import Lock
from typing import List
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel
from fpakman.core import resource, util
from fpakman.core import resource
from fpakman.core.model import ApplicationStatus
from fpakman.util.cache import Cache
from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView, ApplicationViewStatus
class UpdateToggleButton(QToolButton):
def __init__(self, model: dict, root: QWidget, locale_keys: dict, checked: bool = True):
def __init__(self, app_view: ApplicationView, root: QWidget, locale_keys: dict, checked: bool = True):
super(UpdateToggleButton, self).__init__()
self.app = model
self.app_view = app_view
self.root = root
self.setCheckable(True)
self.clicked.connect(self.change_state)
@@ -28,25 +33,24 @@ class UpdateToggleButton(QToolButton):
self.click()
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.root.change_update_state()
class AppsTable(QTableWidget):
def __init__(self, parent: QWidget):
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool):
super(AppsTable, self).__init__()
self.setParent(parent)
self.window = parent
self.column_names = [parent.locale_keys[key].capitalize() for key in ['flatpak.info.name',
'flatpak.info.version',
'manage_window.columns.latest_version',
'flatpak.info.branch',
'flatpak.info.arch',
'flatpak.info.id',
'flatpak.info.origin',
'manage_window.columns.installed',
self.disk_cache = disk_cache
self.column_names = [parent.locale_keys[key].capitalize() for key in ['name',
'version',
'latest_version',
'flatpak.info.description',
'type',
'installed',
'manage_window.columns.update']]
self.setColumnCount(len(self.column_names))
self.setFocusPolicy(Qt.NoFocus)
@@ -56,11 +60,13 @@ class AppsTable(QTableWidget):
self.setHorizontalHeaderLabels(self.column_names)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
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.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
@@ -68,56 +74,85 @@ class AppsTable(QTableWidget):
menu_row = QMenu()
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)
if app.model.installed:
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
action_history.setIcon(QIcon(resource.get_path('img/history.svg')))
action_history.triggered.connect(self._get_app_history)
menu_row.addAction(action_history)
if app.model.has_info():
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"])
action_info.setIcon(QIcon(resource.get_path('img/info.svg')))
action_info.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"])
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.has_history():
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
action_history.setIcon(QIcon(resource.get_path('img/history.svg')))
action_history.triggered.connect(self._get_app_history)
menu_row.addAction(action_history)
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.triggered.connect(self._downgrade_app)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
menu_row.addAction(action_downgrade)
else:
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
action_install.triggered.connect(self._install_app)
menu_row.addAction(action_install)
if app.model.can_be_installed():
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
action_install.triggered.connect(self._install_app)
menu_row.addAction(action_install)
menu_row.adjustSize()
menu_row.popup(QCursor.pos())
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()]
def get_selected_app_icon(self):
def get_selected_app_icon(self) -> QIcon:
return self.item(self.currentRow(), 0).icon()
def _uninstall_app(self):
selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']),
body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys):
self.window.uninstall_app(selected_app['model']['ref'])
self.window.uninstall_app(selected_app)
def _downgrade_app(self):
selected_app = self.get_selected_app()
if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'],
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']),
body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app.model.base_data.name),
locale_keys=self.window.locale_keys):
self.window.downgrade_app(selected_app)
@@ -130,96 +165,100 @@ class AppsTable(QTableWidget):
def _install_app(self):
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()
pixmap = QPixmap()
pixmap.loadFromData(http_response.readAll())
icon = QIcon(pixmap)
self.icon_cache[icon_url] = icon
icon_data = self.icon_cache.get(icon_url)
icon_was_cached = True
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):
if app['model']['icon'] == icon_url:
self.item(idx, 0).setIcon(icon)
self.window.resize_and_center()
break
if app.model.base_data.icon_url == icon_url:
col_name = self.item(idx, 0)
col_name.setIcon(icon_data['icon'])
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.setRowCount(len(apps) if apps 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'])))
self.setRowCount(len(app_views) if app_views else 0)
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)
col_version = QTableWidgetItem()
col_version.setText(app['model']['version'])
col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_version.setToolTip(tooltip)
self.setItem(idx, 1, col_version)
col_version = QLabel(app_v.model.base_data.version)
col_version.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 1, col_version)
col_release = QTableWidgetItem()
col_release.setText(app['model']['latest_version'])
col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_release.setToolTip(tooltip)
self.setItem(idx, 2, col_release)
col_release = QLabel(app_v.get_async_attr('latest_version'))
col_release.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 2, col_release)
if app['model']['version'] and app['model']['latest_version'] and app['model']['version'] < app['model']['latest_version']:
col_release.setForeground(QColor('orange'))
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.setStyleSheet("color: orange")
col_branch = QTableWidgetItem()
col_branch.setText(app['model']['branch'])
col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_branch.setToolTip(tooltip)
self.setItem(idx, 3, col_branch)
col_description = QTableWidgetItem()
col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self._set_col_description(col_description, app_v)
col_arch = QTableWidgetItem()
col_arch.setText(app['model']['arch'])
col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_arch.setToolTip(tooltip)
self.setItem(idx, 4, col_arch)
self.setItem(idx, 3, col_description)
col_id = QTableWidgetItem()
col_id.setText(app['model']['id'])
col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_id.setToolTip(tooltip)
self.setItem(idx, 5, col_id)
col_origin = QTableWidgetItem()
col_origin.setText(app['model']['origin'])
col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
col_origin.setToolTip(tooltip)
self.setItem(idx, 6, col_origin)
col_type = QLabel(app_v.model.get_type())
col_type.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 4, col_type)
col_installed = QLabel()
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross')))))
col_installed.setToolTip(tooltip)
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app_v.model.installed else 'red_cross')))))
col_installed.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 7, col_installed)
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
self.setCellWidget(idx, 8, col_update)
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, 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):
header_horizontal = self.horizontalHeader()
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):
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__()
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name']))
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name))
self.setWindowIcon(app_icon)
layout = QVBoxLayout()
@@ -23,13 +23,13 @@ class HistoryDialog(QDialog):
table_history.verticalHeader().setVisible(False)
table_history.setAlternatingRowColors(True)
table_history.setColumnCount(len(app['commits'][0]))
table_history.setRowCount(len(app['commits']))
table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['commits'][0].keys())])
table_history.setColumnCount(len(app['history'][0]))
table_history.setRowCount(len(app['history']))
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())):
item = QTableWidgetItem()

View File

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

View File

@@ -1,37 +1,62 @@
import time
from datetime import datetime, timedelta
from typing import List
import requests
from PyQt5.QtCore import QThread, pyqtSignal
from fpakman.core import flatpak
from fpakman.core.controller import FlatpakManager
from fpakman.core.controller import ApplicationManager
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.view_model import ApplicationView
class UpdateSelectedApps(QThread):
signal_finished = pyqtSignal()
signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str)
def __init__(self):
def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
super(UpdateSelectedApps, self).__init__()
self.refs_to_update = []
self.apps_to_update = apps_to_update
self.manager = manager
def run(self):
for app_ref in self.refs_to_update:
for output in flatpak.update_and_stream(app_ref):
error = False
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()
if 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):
signal = pyqtSignal(list)
def __init__(self, manager: FlatpakManager):
def __init__(self, manager: ApplicationManager):
super(RefreshApps, self).__init__()
self.manager = manager
@@ -43,85 +68,110 @@ class UninstallApp(QThread):
signal_finished = pyqtSignal()
signal_output = pyqtSignal(str)
def __init__(self):
def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
super(UninstallApp, self).__init__()
self.app_ref = None
self.app = app
self.manager = manager
self.icon_cache = icon_cache
def run(self):
if self.app_ref:
for output in flatpak.uninstall_and_stream(self.app_ref):
if self.app:
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()
if 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()
class DowngradeApp(QThread):
signal_finished = pyqtSignal()
signal_finished = pyqtSignal(bool)
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__()
self.manager = manager
self.app = None
self.root_password = None
self.app = app
self.locale_keys = locale_keys
self.root_password = None
def run(self):
if self.app:
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:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body'])
else:
for output in self.manager.downgrade_app(self.app['model'], self.root_password):
line = output.decode().strip()
if line:
self.signal_output.emit(line)
self.app = None
self.root_password = None
self.signal_finished.emit()
if stream is None:
dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body'])
else:
for output in stream:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
except (requests.exceptions.ConnectionError, NoInternetException):
success = False
self.signal_output.emit(self.locale_keys['internet.required'])
finally:
self.app = None
self.root_password = None
self.signal_finished.emit(success)
class GetAppInfo(QThread):
signal_finished = pyqtSignal(dict)
def __init__(self):
def __init__(self, manager: ApplicationManager, app: ApplicationView = None):
super(GetAppInfo, self).__init__()
self.app = None
self.app = app
self.manager = manager
def run(self):
if self.app:
app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch'])
app_info['name'] = self.app['model']['name']
app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app'
app_info['description'] = self.app['model']['description']
self.signal_finished.emit(app_info)
self.signal_finished.emit(self.manager.get_info(self.app.model))
self.app = None
class GetAppHistory(QThread):
signal_finished = pyqtSignal(dict)
def __init__(self):
def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None):
super(GetAppHistory, self).__init__()
self.app = None
self.app = app
self.manager = manager
self.locale_keys = locale_keys
def run(self):
if self.app:
commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin'])
self.signal_finished.emit({'model': self.app['model'], 'commits': commits})
self.app = None
try:
res = {'model': self.app.model, 'history': self.manager.get_history(self.app.model)}
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):
signal_finished = pyqtSignal(list)
def __init__(self, manager: FlatpakManager):
def __init__(self, manager: ApplicationManager):
super(SearchApps, self).__init__()
self.word = None
self.manager = manager
@@ -138,23 +188,46 @@ class SearchApps(QThread):
class InstallApp(QThread):
signal_finished = pyqtSignal()
signal_finished = pyqtSignal(bool)
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__()
self.app = None
self.app = app
self.manager = manager
self.icon_cache = icon_cache
self.disk_cache = disk_cache
def run(self):
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()
if line:
self.signal_output.emit(line)
self.app = None
self.signal_finished.emit()
error = False
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):
@@ -184,3 +257,38 @@ class AnimateProgress(QThread):
time.sleep(0.05)
self.progress_value = 0
class VerifyModels(QThread):
signal_updates = pyqtSignal()
def __init__(self, apps: List[ApplicationView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
def run(self):
if self.apps:
stop_at = datetime.utcnow() + timedelta(seconds=30)
last_ready = 0
while True:
current_ready = 0
for app in self.apps:
current_ready += 1 if app.model.status == ApplicationStatus.READY else 0
if current_ready > last_ready:
last_ready = current_ready
self.signal_updates.emit()
if current_ready == len(self.apps):
self.signal_updates.emit()
break
if stop_at <= datetime.utcnow():
break
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, \
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar
from fpakman.core import resource, flatpak
from fpakman.core.controller import FlatpakManager
from fpakman.view.qt import dialog
from fpakman.core import resource
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import Application
from fpakman.util.cache import Cache
from fpakman.view.qt.apps_table import AppsTable
from fpakman.view.qt.history import HistoryDialog
from fpakman.view.qt.info import InfoDialog
from fpakman.view.qt.root import is_root, ask_root_password
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels
from fpakman.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400
def __init__(self, locale_keys: dict, manager: FlatpakManager, tray_icon=None):
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, tray_icon=None):
super(ManageWindow, self).__init__()
self.locale_keys = locale_keys
self.manager = manager
@@ -34,9 +35,10 @@ class ManageWindow(QWidget):
self.working = False # restrict the number of threaded actions
self.apps = []
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._check_flatpak_installed()
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
self.setWindowTitle(locale_keys['manage_window.title'])
self.setWindowIcon(self.icon_flathub)
@@ -47,19 +49,16 @@ class ManageWindow(QWidget):
self.toolbar_search = QToolBar()
self.toolbar_search.setStyleSheet("spacing: 0px;")
self.toolbar_search.setContentsMargins(0, 0, 0, 0)
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_search.addWidget(spacer)
self.toolbar_search.addWidget(self._new_spacer())
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.input_search = QLineEdit()
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.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}")
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;")
self.toolbar_search.addWidget(label_pos_search)
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_search.addWidget(spacer)
self.toolbar_search.addWidget(self._new_spacer())
self.layout.addWidget(self.toolbar_search)
toolbar = QToolBar()
@@ -83,23 +80,19 @@ class ManageWindow(QWidget):
self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps)
toolbar.addWidget(self.checkbox_only_apps)
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
toolbar.addWidget(spacer)
toolbar.addWidget(self._new_spacer())
self.label_status = QLabel()
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)
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
toolbar.addWidget(spacer)
toolbar.addWidget(self._new_spacer())
self.bt_refresh = QToolButton()
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.clicked.connect(lambda: self.refresh(clear_output=True))
self.bt_refresh.clicked.connect(lambda: self.refresh(keep_console=False))
toolbar.addWidget(self.bt_refresh)
self.bt_upgrade = QToolButton()
@@ -111,11 +104,22 @@ class ManageWindow(QWidget):
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.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.resize(self.table_apps.size())
self.textarea_output.setStyleSheet("background: black; color: white;")
@@ -123,14 +127,14 @@ class ManageWindow(QWidget):
self.textarea_output.setVisible(False)
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_finished.connect(self._finish_update_selected)
self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh)
self.thread_uninstall = UninstallApp()
self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
self.thread_uninstall.signal_output.connect(self._update_action_output)
self.thread_uninstall.signal_finished.connect(self._finish_uninstall)
@@ -138,46 +142,50 @@ class ManageWindow(QWidget):
self.thread_downgrade.signal_output.connect(self._update_action_output)
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_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_search = SearchApps(self.manager)
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_finished.connect(self._finish_install)
self.thread_animate_progress = AnimateProgress()
self.thread_animate_progress.signal_change.connect(self._update_progress)
self.thread_verify_models = VerifyModels()
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)
self.toolbar_bottom = QToolBar()
self.label_updates = QLabel('')
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
self.toolbar_bottom.addWidget(self.label_updates)
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar_bottom.addWidget(spacer)
self.toolbar_bottom.addWidget(self._new_spacer())
self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(False)
self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar)
spacer = QWidget()
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.toolbar_bottom.addWidget(self._new_spacer())
self.layout.addWidget(self.toolbar_bottom)
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):
if isinstance(e, QWindowStateChangeEvent):
@@ -189,20 +197,7 @@ class ManageWindow(QWidget):
if self.tray_icon:
event.ignore()
self.hide()
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()
self._handle_console_option(False)
def _acquire_lock(self):
@@ -223,61 +218,70 @@ class ManageWindow(QWidget):
self.thread_lock.release()
def _hide_output(self):
self.textarea_output.clear()
def _handle_console(self, checked: bool):
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()
def _show_output(self):
self.textarea_output.clear()
self.textarea_output.show()
def refresh(self, clear_output: bool = True):
def refresh(self, keep_console: bool = True):
if self._acquire_lock():
self._check_flatpak_installed()
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
self.input_search.clear()
if clear_output:
self._hide_output()
if not keep_console:
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
self.thread_refresh.start()
def _finish_refresh(self, apps: List[dict]):
def _finish_refresh(self, apps: List[Application]):
self.update_apps(apps)
self.finish_action()
self._release_lock()
def uninstall_app(self, app_ref: str):
self._check_flatpak_installed()
def uninstall_app(self, app: ApplicationView):
if self._acquire_lock():
self.textarea_output.clear()
self.textarea_output.setVisible(True)
self._begin_action(self.locale_keys['manage_window.status.uninstalling'])
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.uninstalling'], app.model.base_data.name))
self.thread_uninstall.app_ref = app_ref
self.thread_uninstall.app = app
self.thread_uninstall.start()
def _finish_uninstall(self):
self.finish_action()
self._release_lock()
self.refresh(clear_output=False)
self.refresh()
def _finish_downgrade(self):
def _finish_downgrade(self, success: bool):
self.finish_action()
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):
if self.apps:
show_only_apps = True if only_apps == 2 else False
for idx, app in enumerate(self.apps):
hidden = show_only_apps and app['model']['runtime']
for idx, app_v in enumerate(self.apps):
hidden = show_only_apps and app_v.model.is_library()
self.table_apps.setRowHidden(idx, hidden)
app['visible'] = not hidden
app_v.visible = not hidden
self.change_update_state()
self.table_apps.change_headers_policy(QHeaderView.Stretch)
@@ -288,31 +292,32 @@ class ManageWindow(QWidget):
enable_bt_update = False
app_updates, runtime_updates = 0, 0
app_updates, library_updates = 0, 0
for app in self.apps:
if app['model']['update']:
if app['model']['runtime']:
runtime_updates += 1
for app_v in self.apps:
if app_v.model.update:
if app_v.model.runtime:
library_updates += 1
else:
app_updates += 1
total_updates = app_updates + runtime_updates
total_updates = app_updates + library_updates
if total_updates > 0:
self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates))
self.label_updates.setToolTip('{} {} | {} runtimes'.format(app_updates,
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
runtime_updates))
self.label_updates.setToolTip('{} {} | {} {}'.format(app_updates,
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
library_updates,
self.locale_keys['others'].lower()))
else:
self.label_updates.setText('')
for app in self.apps:
if app['visible'] and app['update_checked']:
for app_v in self.apps:
if app_v.visible and app_v.update_checked:
enable_bt_update = True
break
self.bt_upgrade.setEnabled(enable_bt_update)
self.tray_icon.notify_updates([app['model'] for app in self.apps if app['model']['update']])
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
def centralize(self):
geo = self.frameGeometry()
@@ -321,20 +326,17 @@ class ManageWindow(QWidget):
geo.moveCenter(center_point)
self.move(geo.topLeft())
def update_apps(self, apps: List[dict]):
self._check_flatpak_installed()
def update_apps(self, apps: List[Application]):
self.apps = []
napps = 0 # number of apps (not runtimes)
napps = 0 # number of apps (not libraries)
if apps:
for app in apps:
app_model = {'model': app,
'update_checked': app['update'],
'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()}
app_model = ApplicationView(model=app,
visible=(not app.is_library()) 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)
if napps == 0:
@@ -349,6 +351,9 @@ class ManageWindow(QWidget):
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
self.resize_and_center()
self.thread_verify_models.apps = self.apps
self.thread_verify_models.start()
def resize_and_center(self):
new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05
self.resize(new_width, self.height())
@@ -359,55 +364,59 @@ class ManageWindow(QWidget):
if self._acquire_lock():
if self.apps:
to_update = [pak['model']['ref'] for pak in self.apps if pak['visible'] and pak['update_checked']]
to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked]
if to_update:
self.textarea_output.clear()
self.textarea_output.setVisible(True)
self._handle_console_option(True)
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()
def _finish_update_selected(self):
def _finish_update_selected(self, success: bool):
self.finish_action()
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):
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.start()
self.ref_progress_bar.setVisible(True)
self.progress_bar.setValue(50)
self.label_status.setText(action_label + "...")
self.toolbar_search.setVisible(False)
self.bt_upgrade.setEnabled(False)
self.bt_refresh.setEnabled(False)
self.checkbox_only_apps.setEnabled(False)
self.table_apps.setEnabled(False)
def finish_action(self, clear_search: bool = True):
self.thread_animate_progress.stop = True
if keep_search:
self.toolbar_search.setEnabled(False)
else:
self.toolbar_search.setVisible(False)
def finish_action(self):
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.setVisible(False)
self.bt_refresh.setEnabled(True)
self.toolbar_search.setVisible(True)
self.checkbox_only_apps.setEnabled(True)
self.table_apps.setEnabled(True)
self.input_search.setEnabled(True)
self.label_status.setText('')
self.toolbar_search.setVisible(True)
self.toolbar_search.setEnabled(True)
if clear_search:
self.input_search.setText('')
def downgrade_app(self, app: dict):
self._check_flatpak_installed()
def downgrade_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
@@ -419,9 +428,8 @@ class ManageWindow(QWidget):
self._release_lock()
return
self.textarea_output.clear()
self.textarea_output.setVisible(True)
self._begin_action(self.locale_keys['manage_window.status.downgrading'])
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name))
self.thread_downgrade.app = app
self.thread_downgrade.root_password = pwd
@@ -430,8 +438,7 @@ class ManageWindow(QWidget):
def get_app_info(self, app: dict):
if self._acquire_lock():
self.textarea_output.clear()
self.textarea_output.setVisible(False)
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.info'])
self.thread_get_info.app = app
@@ -439,8 +446,7 @@ class ManageWindow(QWidget):
def get_app_history(self, app: dict):
if self._acquire_lock():
self.textarea_output.clear()
self.textarea_output.setVisible(False)
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.history'])
self.thread_get_history.app = app
@@ -457,42 +463,47 @@ class ManageWindow(QWidget):
self._release_lock()
self.finish_action()
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):
word = self.input_search.text().strip()
if word and self._acquire_lock():
self.textarea_output.clear()
self.textarea_output.setVisible(False)
self._begin_action(self.locale_keys['manage_window.status.searching'])
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.searching'], keep_search=True)
self.thread_search.word = word
self.thread_search.start()
def _finish_search(self, apps_found: List[dict]):
def _finish_search(self, apps_found: List[Application]):
self._release_lock()
self.finish_action(clear_search=False)
self.finish_action()
self.update_apps(apps_found)
def install_app(self, app: dict):
self._check_flatpak_installed()
def install_app(self, app: ApplicationView):
if self._acquire_lock():
self._begin_action(self.locale_keys['manage_window.status.installing'])
self._show_output()
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.installing'], app.model.base_data.name))
self.thread_install.app = app
self.thread_install.start()
def _finish_install(self):
def _finish_install(self, success: bool):
self.input_search.setText('')
self.finish_action()
self._release_lock()
self.refresh(clear_output=False)
if success:
self.refresh()
else:
self.checkbox_console.setChecked(True)
def _update_progress(self, value: int):
self.progress_bar.setValue(value)