mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 01:14:15 +02:00
0.3.1
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.3.1'
|
||||
__app_name__ = 'fpakman'
|
||||
|
||||
@@ -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_())
|
||||
|
||||
2
fpakman/core/constants.py
Normal file
2
fpakman/core/constants.py
Normal file
@@ -0,0 +1,2 @@
|
||||
FLATHUB_URL = 'https://flathub.org'
|
||||
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||
@@ -1,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
79
fpakman/core/disk.py
Normal 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)
|
||||
3
fpakman/core/exception.py
Normal file
3
fpakman/core/exception.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
class NoInternetException(Exception):
|
||||
pass
|
||||
@@ -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
150
fpakman/core/model.py
Normal 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
12
fpakman/core/structure.py
Normal 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)
|
||||
@@ -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
140
fpakman/core/worker.py
Normal 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 = []
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
0
fpakman/util/__init__.py
Normal file
63
fpakman/util/cache.py
Normal file
63
fpakman/util/cache.py
Normal 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
25
fpakman/util/memory.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import time
|
||||
from threading import Thread
|
||||
from typing import List
|
||||
|
||||
from fpakman.util.cache import Cache
|
||||
import gc
|
||||
|
||||
|
||||
class CacheCleaner(Thread):
|
||||
|
||||
def __init__(self, caches: List[Cache], check_interval: int = 15):
|
||||
super(CacheCleaner, self).__init__(daemon=True)
|
||||
self.caches = [c for c in caches if c.is_enabled()]
|
||||
self.check_interval = check_interval
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.caches:
|
||||
while True:
|
||||
for cache in self.caches:
|
||||
cache.clean_expired()
|
||||
|
||||
gc.collect()
|
||||
time.sleep(self.check_interval)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
27
fpakman/view/qt/view_model.py
Normal file
27
fpakman/view/qt/view_model.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from enum import Enum
|
||||
|
||||
from fpakman.util import util
|
||||
from fpakman.core.model import Application, ApplicationStatus, FlatpakApplication
|
||||
|
||||
|
||||
class ApplicationViewStatus(Enum):
|
||||
LOADING = 0
|
||||
READY = 1
|
||||
|
||||
|
||||
class ApplicationView:
|
||||
|
||||
def __init__(self, model: Application, visible: bool = True):
|
||||
self.model = model
|
||||
self.update_checked = model.update
|
||||
self.visible = visible
|
||||
self.status = ApplicationViewStatus.LOADING
|
||||
|
||||
def get_async_attr(self, attr: str, strip_html: bool = False, default: str = '...'):
|
||||
|
||||
if isinstance(self.model, FlatpakApplication) and self.model.runtime:
|
||||
res = getattr(self.model.base_data, attr)
|
||||
else:
|
||||
res = getattr(self.model.base_data, attr) if self.model.status == ApplicationStatus.READY and getattr(self.model.base_data, attr) else default
|
||||
|
||||
return util.strip_html(res) if res and strip_html else res
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user