mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-06 21:44:16 +02:00
supporting snaps
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -4,6 +4,25 @@ 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.4.0]
|
||||
### Features
|
||||
- supporting snaps
|
||||
- search filters by application type and updates
|
||||
- "Refresh" option when right-clicking an installed snap application (see **Comments**)
|
||||
- snap / flatpak usage can be enabled / disabled via this new environment variables and arguments: FPAKMAN_FLATPAK (--flatpak), FPAKMAN_SNAP (--snap)
|
||||
### Improvements
|
||||
- automatically shows all updates after refreshing
|
||||
- more accurate search results.
|
||||
- system notifications when an application is installed, removed or downgraded. Also when an error occurs.
|
||||
- showing management panel when right-clicking the tray icon.
|
||||
- "Updates" label replaced by an exclamation icon and moved to the right.
|
||||
- new environments variables / arguments associated with performance: FPAKMAN_DOWNLOAD_ICONS (--download-icons), FPAKMAN_CHECK_PACKAGING_ONCE (--check-packaging-once)
|
||||
- minor GUI improvements
|
||||
### Comments
|
||||
- currently snap daemon (2.40) automatically upgrades your installed applications in background. Although it's possible to check for new updates
|
||||
programmatically, it requires root access and would mess up with the user experience if every 5 minutes the application asked for the password. But not to let the
|
||||
user with empty hands, it was added a "Refresh" option when right-clicking an installed snap application. It will update the application if its not already updated by the daemon.
|
||||
|
||||
## [0.3.1] - 2019-07-13
|
||||
### Improvements
|
||||
- Console output now is optional and not shown by default
|
||||
|
||||
13
README.md
13
README.md
@@ -1,6 +1,6 @@
|
||||
## fpakman
|
||||
|
||||
Non-official graphical user interface for Flatpak application management. It is a tray icon that let the user known when new updates are available and
|
||||
Non-official graphical user interface for Flatpak / Snap application management. It is a tray icon that let the user known when new updates are available and
|
||||
an application management panel where you can search, update, install and uninstall applications.
|
||||
|
||||
### Developed with:
|
||||
@@ -48,8 +48,19 @@ You can change some application settings via environment variables or arguments
|
||||
- **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).
|
||||
- **FPAKMAN_DOWNLOAD_ICONS**: Enables / disables app icons download. It may improve the application speed depending on how applications data are being retrieved. Use **0** (disable) or **1** (enable, default).
|
||||
- **FPAKMAN_FLATPAK**: enables / disables flatpak usage. Use **0** (disable) or **1** (enabled, default)
|
||||
- **FPAKMAN_SNAP**: enables / disables snap usage. Use **0** (disable) or **1** (enabled, default)
|
||||
- **FPAKMAN_CHECK_PACKAGING_ONCE**: If the available supported packaging types should be checked ONLY once. It improves the application speed if enabled, but can generate errors if you uninstall any packaging technology while using it, and every time a supported packaging type is installed it will only be available after a restart. Use **0** (disable, default) or **1** (enable).
|
||||
|
||||
### How to improve the application performance
|
||||
- If you don't care about a specific packaging technology and don't want **fpakman** to deal with it, just disable it via the specific argument or environment variable. For instance, if I don't care
|
||||
about **snaps**, I can initialize the application setting "snap=0" (**fpakman --snap=0**). This will improve the application response time, since it won't need to do any verifications associated
|
||||
with the technology that I don't care every time an action is executed.
|
||||
- If you don't care about restarting **fpakman** every time a new supported packaging technology is installed, set "check-packaging-once=1" (**fpakman --check-packaging-once=1**). This can reduce the application response time up to 80% in some scenarios, since it won't need to recheck if the packaging type is available for every action you request.
|
||||
- If you don't mind to see the applications icons, you can set "download-icons=0" (**fpakman --download-icons=0**). The application may have a slight response improvement, since it will reduce the parallelism within it.
|
||||
|
||||
### Roadmap
|
||||
- Support for other packaging technologies
|
||||
- Memory and performance improvements
|
||||
- Design
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
__version__ = '0.3.1'
|
||||
__version__ = '0.4.0'
|
||||
__app_name__ = 'fpakman'
|
||||
|
||||
import os
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from colorama import Fore
|
||||
|
||||
from fpakman import __version__, __app_name__
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.controller import GenericApplicationManager
|
||||
from fpakman.core.disk import DiskCacheLoaderFactory
|
||||
from fpakman.core.structure import prepare_folder_structure
|
||||
from fpakman.core.flatpak.constants import FLATPAK_CACHE_PATH
|
||||
from fpakman.core.flatpak.controller import FlatpakManager
|
||||
from fpakman.core.flatpak.model import FlatpakApplication
|
||||
from fpakman.core.snap.constants import SNAP_CACHE_PATH
|
||||
from fpakman.core.snap.controller import SnapManager
|
||||
from fpakman.core.snap.model import SnapApplication
|
||||
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
|
||||
@@ -31,8 +38,12 @@ parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('
|
||||
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='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')
|
||||
parser.add_argument('-n', '--update-notification', action="store", choices=[0, 1], 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", choices=[0, 1], 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')
|
||||
parser.add_argument('-di', '--download-icons', action="store", choices=[0, 1], default=os.getenv('FPAKMAN_DOWNLOAD_ICONS', 1), type=int, help='Enables / disables app icons download. It may improve the application speed, depending of how applications data are retrieved by their extensions.')
|
||||
parser.add_argument('--flatpak', action="store", default=os.getenv('FPAKMAN_FLATPAK', 1), choices=[0, 1], type=int, help='Enables / disables flatpak usage. Default: %(default)s')
|
||||
parser.add_argument('--snap', action="store", default=os.getenv('FPAKMAN_SNAP', 1), choices=[0, 1], type=int, help='Enables / disables snap usage. Default: %(default)s')
|
||||
parser.add_argument('-co', '--check-packaging-once', action="store", default=os.getenv('FPAKMAN_CHECK_PACKAGING_ONCE', 0), choices=[0, 1], type=int, help='If the available supported packaging types should be checked ONLY once. It improves the application speed if enabled, but can generate errors if you uninstall any packaging technology while using it, and every time a supported packaging type is installed it will only be available after a restart. Default: %(default)s')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cache_exp < 0:
|
||||
@@ -49,33 +60,66 @@ if args.check_interval <= 0:
|
||||
log_msg("'check-interval' set as '{}'. It must be >= 0. Aborting...".format(args.check_interval), Fore.RED)
|
||||
exit(1)
|
||||
|
||||
if not args.flatpak:
|
||||
log_msg("'flatpak' is disabled.", Fore.YELLOW)
|
||||
|
||||
if not args.snap:
|
||||
log_msg("'snap' is disabled.", Fore.YELLOW)
|
||||
|
||||
if args.update_notification == 0:
|
||||
log_msg('updates notifications are disabled', Fore.YELLOW)
|
||||
|
||||
if args.download_icons == 0:
|
||||
log_msg("'download-icons' is disabled", Fore.YELLOW)
|
||||
|
||||
if args.check_packaging_once == 1:
|
||||
log_msg("'check-packaging-once' is enabled", Fore.YELLOW)
|
||||
|
||||
locale_keys = util.get_locale_keys(args.locale)
|
||||
|
||||
prepare_folder_structure(disk_cache=args.disk_cache)
|
||||
|
||||
http_session = requests.Session()
|
||||
caches = []
|
||||
flatpak_api_cache = Cache(expiration_time=args.cache_exp)
|
||||
caches.append(flatpak_api_cache)
|
||||
cache_map = {}
|
||||
managers = []
|
||||
|
||||
if args.flatpak:
|
||||
flatpak_api_cache = Cache(expiration_time=args.cache_exp)
|
||||
cache_map[FlatpakApplication] = flatpak_api_cache
|
||||
managers.append(FlatpakManager(app_args=args, api_cache=flatpak_api_cache, disk_cache=args.disk_cache, http_session=http_session))
|
||||
caches.append(flatpak_api_cache)
|
||||
|
||||
if args.disk_cache:
|
||||
Path(FLATPAK_CACHE_PATH).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.snap:
|
||||
snap_api_cache = Cache(expiration_time=args.cache_exp)
|
||||
cache_map[SnapApplication] = snap_api_cache
|
||||
managers.append(SnapManager(app_args=args, disk_cache=args.disk_cache, api_cache=snap_api_cache, http_session=http_session))
|
||||
caches.append(snap_api_cache)
|
||||
|
||||
if args.disk_cache:
|
||||
Path(SNAP_CACHE_PATH).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
icon_cache = Cache(expiration_time=args.icon_exp)
|
||||
caches.append(icon_cache)
|
||||
|
||||
disk_loader_factory = DiskCacheLoaderFactory(disk_cache=args.disk_cache, cache_map=cache_map)
|
||||
manager = GenericApplicationManager(managers, disk_loader_factory=disk_loader_factory, app_args=args)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName(__app_name__)
|
||||
app.setApplicationVersion(__version__)
|
||||
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg')))
|
||||
|
||||
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))
|
||||
update_notification=bool(args.update_notification),
|
||||
download_icons=args.download_icons,
|
||||
screen_size=app.primaryScreen().size())
|
||||
|
||||
trayIcon.show()
|
||||
|
||||
CacheCleaner(caches).start()
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
FLATHUB_URL = 'https://flathub.org'
|
||||
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||
from pathlib import Path
|
||||
|
||||
from fpakman import __app_name__
|
||||
|
||||
HOME_PATH = Path.home()
|
||||
CACHE_PATH = '{}/.cache/{}'.format(HOME_PATH, __app_name__)
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from argparse import Namespace
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
import requests
|
||||
|
||||
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
|
||||
from fpakman.core.model import Application
|
||||
from fpakman.core.system import FpakmanProcess
|
||||
|
||||
|
||||
class ApplicationManager(ABC):
|
||||
|
||||
def __init__(self, app_args):
|
||||
self.app_args = app_args
|
||||
|
||||
@abstractmethod
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[Application]:
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[Application]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool) -> List[Application]:
|
||||
def read_installed(self, disk_loader: DiskCacheLoader) -> List[Application]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def downgrade_app(self, app: Application, root_password: str):
|
||||
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -36,15 +35,15 @@ class ApplicationManager(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_and_stream(self, app: Application):
|
||||
def update_and_stream(self, app: Application) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def uninstall_and_stream(self, app: Application):
|
||||
def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_app_type(self) -> str:
|
||||
def get_app_type(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -56,7 +55,7 @@ class ApplicationManager(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def install_and_stream(self, app: Application):
|
||||
def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -67,194 +66,127 @@ class ApplicationManager(ABC):
|
||||
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def requires_root(self, action: str, app: Application):
|
||||
pass
|
||||
|
||||
from fpakman.core.worker import FlatpakAsyncDataLoaderManager
|
||||
@abstractmethod
|
||||
def refresh(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self):
|
||||
"""
|
||||
Callback executed before the ApplicationManager starts to work.
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FlatpakManager(ApplicationManager):
|
||||
class GenericApplicationManager(ApplicationManager):
|
||||
|
||||
def __init__(self, api_cache: Cache, disk_cache: bool):
|
||||
self.api_cache = api_cache
|
||||
self.http_session = requests.Session()
|
||||
def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory, app_args: Namespace):
|
||||
super(ApplicationManager, self).__init__()
|
||||
self.managers = managers
|
||||
self.map = {m.get_app_type(): m for m in self.managers}
|
||||
self.disk_loader_factory = disk_loader_factory
|
||||
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()
|
||||
self._enabled_map = {} if app_args.check_packaging_once else None
|
||||
self.prepare()
|
||||
|
||||
def get_app_type(self):
|
||||
return FlatpakApplication
|
||||
def _sort(self, apps: List[Application], word: str) -> List[Application]:
|
||||
|
||||
def _map_to_model(self, app: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
|
||||
exact_name_matches, contains_name_matches, desc_name_matches, others = [], [], [], []
|
||||
|
||||
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
|
||||
for app in apps:
|
||||
lower_name = app.base_data.name.lower()
|
||||
|
||||
api_data = self.api_cache.get(app['id'])
|
||||
|
||||
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
|
||||
|
||||
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)
|
||||
|
||||
else:
|
||||
model.fill_cached_data(api_data)
|
||||
|
||||
return model
|
||||
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
|
||||
if word == lower_name:
|
||||
exact_name_matches.append(app)
|
||||
elif word in lower_name:
|
||||
contains_name_matches.append(app)
|
||||
elif app.base_data.description and word in app.base_data.description.lower():
|
||||
desc_name_matches.append(app)
|
||||
else:
|
||||
others.append(app)
|
||||
|
||||
res = []
|
||||
apps_found = flatpak.search(word)
|
||||
|
||||
if apps_found:
|
||||
already_read = set()
|
||||
installed_apps = self.read_installed(disk_loader=disk_loader, keep_workers=True)
|
||||
|
||||
if installed_apps:
|
||||
for app_found in apps_found:
|
||||
for installed_app in installed_apps:
|
||||
if app_found['id'] == installed_app.base_data.id:
|
||||
res.append(installed_app)
|
||||
already_read.add(app_found['id'])
|
||||
|
||||
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()
|
||||
for app_list in (exact_name_matches, contains_name_matches, desc_name_matches, others):
|
||||
app_list.sort(key=lambda a: a.base_data.name.lower())
|
||||
res.extend(app_list)
|
||||
|
||||
return res
|
||||
|
||||
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool = False) -> List[FlatpakApplication]:
|
||||
def _is_enabled(self, man: ApplicationManager):
|
||||
|
||||
if self._enabled_map is not None:
|
||||
enabled = self._enabled_map.get(man.get_app_type())
|
||||
|
||||
if enabled is None:
|
||||
enabled = man.is_enabled()
|
||||
self._enabled_map[man.get_app_type()] = enabled
|
||||
|
||||
return enabled
|
||||
else:
|
||||
return man.is_enabled()
|
||||
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> Dict[str, List[Application]]:
|
||||
self.lock_read.acquire()
|
||||
try:
|
||||
res = {'installed': [], 'new': []}
|
||||
|
||||
norm_word = word.strip().lower()
|
||||
disk_loader = None
|
||||
|
||||
for man in self.managers:
|
||||
if self._is_enabled(man):
|
||||
if not disk_loader:
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
|
||||
apps_found = man.search(word=norm_word, disk_loader=disk_loader)
|
||||
res['installed'].extend(apps_found['installed'])
|
||||
res['new'].extend(apps_found['new'])
|
||||
|
||||
disk_loader.stop = True
|
||||
disk_loader.join()
|
||||
|
||||
for key in res:
|
||||
res[key] = self._sort(res[key], norm_word)
|
||||
|
||||
return res
|
||||
finally:
|
||||
self.lock_read.release()
|
||||
|
||||
def read_installed(self, disk_loader: DiskCacheLoader = None) -> List[Application]:
|
||||
self.lock_read.acquire()
|
||||
|
||||
try:
|
||||
installed = flatpak.list_installed()
|
||||
installed = []
|
||||
|
||||
if installed:
|
||||
installed.sort(key=lambda p: p['name'].lower())
|
||||
disk_loader = None
|
||||
|
||||
available_updates = flatpak.list_updates_as_str()
|
||||
for man in self.managers:
|
||||
if self._is_enabled(man):
|
||||
if not disk_loader:
|
||||
disk_loader = self.disk_loader_factory.new()
|
||||
disk_loader.start()
|
||||
|
||||
models = []
|
||||
installed.extend(man.read_installed(disk_loader=disk_loader))
|
||||
|
||||
for app in installed:
|
||||
model = self._map_to_model(app, True, disk_loader)
|
||||
model.update = app['id'] in available_updates
|
||||
models.append(model)
|
||||
disk_loader.stop = True
|
||||
disk_loader.join()
|
||||
|
||||
if not keep_workers:
|
||||
self.async_data_loader.stop_current_workers()
|
||||
|
||||
return models
|
||||
|
||||
return []
|
||||
installed.sort(key=lambda a: a.base_data.name.lower())
|
||||
|
||||
return installed
|
||||
finally:
|
||||
self.lock_read.release()
|
||||
|
||||
def can_downgrade(self):
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man and man.can_downgrade():
|
||||
@@ -268,23 +200,23 @@ class GenericApplicationManager(ApplicationManager):
|
||||
if man:
|
||||
return man.clean_cache_for(app)
|
||||
|
||||
def update_and_stream(self, app: Application):
|
||||
def update_and_stream(self, app: Application) -> FpakmanProcess:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.update_and_stream(app)
|
||||
|
||||
def uninstall_and_stream(self, app: Application):
|
||||
def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.uninstall_and_stream(app)
|
||||
return man.uninstall_and_stream(app, root_password)
|
||||
|
||||
def install_and_stream(self, app: Application):
|
||||
def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.install_and_stream(app)
|
||||
return man.install_and_stream(app, root_password)
|
||||
|
||||
def get_info(self, app: Application):
|
||||
man = self._get_manager_for(app)
|
||||
@@ -306,7 +238,7 @@ class GenericApplicationManager(ApplicationManager):
|
||||
|
||||
def _get_manager_for(self, app: Application) -> ApplicationManager:
|
||||
man = self.map[app.__class__]
|
||||
return man if man and man.is_enabled() else None
|
||||
return man if man and self._is_enabled(man) 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():
|
||||
@@ -314,3 +246,22 @@ class GenericApplicationManager(ApplicationManager):
|
||||
|
||||
if man:
|
||||
return man.cache_to_disk(app, icon_bytes=icon_bytes, only_icon=only_icon)
|
||||
|
||||
def requires_root(self, action: str, app: Application):
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.requires_root(action, app)
|
||||
|
||||
def refresh(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
man = self._get_manager_for(app)
|
||||
|
||||
if man:
|
||||
return man.refresh(app, root_password)
|
||||
|
||||
def prepare(self):
|
||||
|
||||
if self.managers:
|
||||
for man in self.managers:
|
||||
if self._is_enabled(man):
|
||||
man.prepare()
|
||||
|
||||
@@ -2,20 +2,20 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from threading import Thread, Lock
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from fpakman.core.model import Application, FlatpakApplication
|
||||
from fpakman.core.model import Application
|
||||
from fpakman.util.cache import Cache
|
||||
|
||||
|
||||
class DiskCacheLoader(Thread):
|
||||
|
||||
def __init__(self, enabled: bool, flatpak_api_cache: Cache, apps: List[Application] = []):
|
||||
def __init__(self, enabled: bool, cache_map: Dict[type, 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.cache_map = cache_map
|
||||
self.enabled = enabled
|
||||
|
||||
def run(self):
|
||||
@@ -44,19 +44,17 @@ class DiskCacheLoader(Thread):
|
||||
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)
|
||||
self.cache_map.get(app.__class__).add_non_existing(app.base_data.id, cached_data)
|
||||
|
||||
|
||||
class DiskCacheLoaderFactory:
|
||||
|
||||
def __init__(self, disk_cache: bool, flatpak_api_cache: Cache):
|
||||
def __init__(self, disk_cache: bool, cache_map: Dict[type, Cache]):
|
||||
self.disk_cache = disk_cache
|
||||
self.flatpak_api_cache = flatpak_api_cache
|
||||
self.cache_map = cache_map
|
||||
|
||||
def new(self):
|
||||
return DiskCacheLoader(enabled=self.disk_cache, flatpak_api_cache=self.flatpak_api_cache)
|
||||
return DiskCacheLoader(enabled=self.disk_cache, cache_map=self.cache_map)
|
||||
|
||||
|
||||
def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False):
|
||||
@@ -65,12 +63,10 @@ def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False):
|
||||
|
||||
if not only_icon:
|
||||
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
|
||||
data = app.get_data_to_cache()
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
0
fpakman/core/flatpak/__init__.py
Normal file
0
fpakman/core/flatpak/__init__.py
Normal file
5
fpakman/core/flatpak/constants.py
Normal file
5
fpakman/core/flatpak/constants.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from fpakman.core.constants import CACHE_PATH
|
||||
|
||||
FLATHUB_URL = 'https://flathub.org'
|
||||
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
|
||||
FLATPAK_CACHE_PATH = '{}/flatpak/installed'.format(CACHE_PATH)
|
||||
150
fpakman/core/flatpak/controller.py
Normal file
150
fpakman/core/flatpak/controller.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import os
|
||||
import shutil
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
from typing import List, Dict
|
||||
|
||||
from fpakman.core import disk
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.disk import DiskCacheLoader
|
||||
from fpakman.core.flatpak import flatpak
|
||||
from fpakman.core.flatpak.model import FlatpakApplication
|
||||
from fpakman.core.flatpak.worker import FlatpakAsyncDataLoader
|
||||
from fpakman.core.model import ApplicationData
|
||||
from fpakman.core.system import FpakmanProcess
|
||||
from fpakman.util.cache import Cache
|
||||
|
||||
|
||||
class FlatpakManager(ApplicationManager):
|
||||
|
||||
def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session):
|
||||
super(FlatpakManager, self).__init__(app_args=app_args)
|
||||
self.api_cache = api_cache
|
||||
self.http_session = http_session
|
||||
self.disk_cache = disk_cache
|
||||
|
||||
def get_app_type(self):
|
||||
return FlatpakApplication
|
||||
|
||||
def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
|
||||
|
||||
app = FlatpakApplication(arch=app_json.get('arch'),
|
||||
branch=app_json.get('branch'),
|
||||
origin=app_json.get('origin'),
|
||||
runtime=app_json.get('runtime'),
|
||||
ref=app_json.get('ref'),
|
||||
commit=app_json.get('commit'),
|
||||
base_data=ApplicationData(id=app_json.get('id'),
|
||||
name=app_json.get('name'),
|
||||
version=app_json.get('version'),
|
||||
latest_version=app_json.get('latest_version')))
|
||||
app.installed = installed
|
||||
|
||||
api_data = self.api_cache.get(app_json['id'])
|
||||
|
||||
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
|
||||
|
||||
if not api_data or expired_data:
|
||||
if not app_json['runtime']:
|
||||
disk_loader.add(app) # preloading cached disk data
|
||||
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session).start()
|
||||
|
||||
else:
|
||||
app.fill_cached_data(api_data)
|
||||
|
||||
return app
|
||||
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[FlatpakApplication]]:
|
||||
|
||||
res = {'installed': [], 'new': []}
|
||||
apps_found = flatpak.search(word)
|
||||
|
||||
if apps_found:
|
||||
already_read = set()
|
||||
installed_apps = self.read_installed(disk_loader=disk_loader)
|
||||
|
||||
if installed_apps:
|
||||
for app_found in apps_found:
|
||||
for installed_app in installed_apps:
|
||||
if app_found['id'] == installed_app.base_data.id:
|
||||
res['installed'].append(installed_app)
|
||||
already_read.add(app_found['id'])
|
||||
|
||||
if len(apps_found) > len(already_read):
|
||||
for app_found in apps_found:
|
||||
if app_found['id'] not in already_read:
|
||||
res['new'].append(self._map_to_model(app_found, False, disk_loader))
|
||||
|
||||
return res
|
||||
|
||||
def read_installed(self, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]:
|
||||
installed = flatpak.list_installed()
|
||||
models = []
|
||||
|
||||
if installed:
|
||||
|
||||
available_updates = flatpak.list_updates_as_str()
|
||||
|
||||
for app_json in installed:
|
||||
model = self._map_to_model(app_json=app_json, installed=True, disk_loader=disk_loader)
|
||||
model.update = app_json['id'] in available_updates
|
||||
models.append(model)
|
||||
|
||||
return models
|
||||
|
||||
def can_downgrade(self):
|
||||
return True
|
||||
|
||||
def downgrade_app(self, app: FlatpakApplication, root_password: str) -> FpakmanProcess:
|
||||
|
||||
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 FpakmanProcess(subproc=flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password),
|
||||
success_phrase='Updates complete.')
|
||||
|
||||
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) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=flatpak.update_and_stream(app.ref))
|
||||
|
||||
def uninstall_and_stream(self, app: FlatpakApplication, root_password: str = None) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=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, root_password: str) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=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)
|
||||
|
||||
def requires_root(self, action: str, app: FlatpakApplication):
|
||||
return action == 'downgrade'
|
||||
|
||||
def refresh(self, app: FlatpakApplication, root_password: str) -> FpakmanProcess:
|
||||
raise Exception("'refresh' is not supported for {}".format(app.__class__.__name__))
|
||||
|
||||
def prepare(self):
|
||||
flatpak.set_default_remotes()
|
||||
@@ -4,7 +4,6 @@ from typing import List
|
||||
|
||||
from fpakman.core import system
|
||||
from fpakman.core.exception import NoInternetException
|
||||
from fpakman.core.model import Application
|
||||
|
||||
BASE_CMD = 'flatpak'
|
||||
|
||||
@@ -115,16 +114,8 @@ def list_updates_as_str():
|
||||
return system.run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
|
||||
|
||||
|
||||
def downgrade_and_stream(app_ref: str, commit: str, root_password: str):
|
||||
|
||||
pwdin, downgrade_cmd = None, []
|
||||
|
||||
if root_password is not None:
|
||||
downgrade_cmd.extend(['sudo', '-S'])
|
||||
pwdin = system.stream_cmd(['echo', root_password])
|
||||
|
||||
downgrade_cmd.extend([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'])
|
||||
return subprocess.Popen(downgrade_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout
|
||||
def downgrade_and_stream(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
|
||||
return system.cmd_as_root([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password)
|
||||
|
||||
|
||||
def get_app_commits(app_ref: str, origin: str) -> List[str]:
|
||||
62
fpakman/core/flatpak/model.py
Normal file
62
fpakman/core/flatpak/model.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.flatpak.constants import FLATPAK_CACHE_PATH
|
||||
from fpakman.core.model import Application, ApplicationData
|
||||
|
||||
|
||||
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 self.installed
|
||||
|
||||
def can_be_downgraded(self):
|
||||
return True
|
||||
|
||||
def can_be_uninstalled(self):
|
||||
return True
|
||||
|
||||
def can_be_installed(self):
|
||||
return True
|
||||
|
||||
def get_type(self):
|
||||
return 'flatpak'
|
||||
|
||||
def can_be_refreshed(self):
|
||||
return False
|
||||
|
||||
def get_default_icon_path(self):
|
||||
return resource.get_path('img/flathub.svg')
|
||||
|
||||
def is_library(self):
|
||||
return self.runtime
|
||||
|
||||
def get_disk_cache_path(self):
|
||||
return '{}/{}'.format(FLATPAK_CACHE_PATH, self.base_data.id)
|
||||
|
||||
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])
|
||||
84
fpakman/core/flatpak/worker.py
Normal file
84
fpakman/core/flatpak/worker.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from colorama import Fore
|
||||
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
|
||||
from fpakman.core.flatpak.model import FlatpakApplication
|
||||
from fpakman.core.model import ApplicationStatus
|
||||
from fpakman.core.worker import AsyncDataLoader
|
||||
from fpakman.util.cache import Cache
|
||||
|
||||
|
||||
class FlatpakAsyncDataLoader(AsyncDataLoader):
|
||||
|
||||
def __init__(self, app: FlatpakApplication, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 2, timeout: int = 30):
|
||||
super(FlatpakAsyncDataLoader, self).__init__(app=app)
|
||||
self.manager = manager
|
||||
self.http_session = http_session
|
||||
self.attempts = attempts
|
||||
self.api_cache = api_cache
|
||||
self.to_persist = {} # stores all data loaded by the instance
|
||||
self.timeout = timeout
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
self.app.status = ApplicationStatus.LOADING_DATA
|
||||
|
||||
for _ in range(0, self.attempts):
|
||||
try:
|
||||
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.base_data.id), timeout=self.timeout)
|
||||
|
||||
if res.status_code == 200 and res.text:
|
||||
data = res.json()
|
||||
|
||||
if not self.app.base_data.version:
|
||||
self.app.base_data.version = data.get('version')
|
||||
|
||||
if not self.app.base_data.name:
|
||||
self.app.base_data.name = data.get('name')
|
||||
|
||||
self.app.base_data.description = data.get('description', data.get('summary', None))
|
||||
self.app.base_data.icon_url = data.get('iconMobileUrl', None)
|
||||
self.app.base_data.latest_version = data.get('currentReleaseVersion', self.app.base_data.version)
|
||||
|
||||
if not self.app.base_data.version and self.app.base_data.latest_version:
|
||||
self.app.base_data.version = self.app.base_data.latest_version
|
||||
|
||||
if self.app.base_data.icon_url and self.app.base_data.icon_url.startswith('/'):
|
||||
self.app.base_data.icon_url = FLATHUB_URL + self.app.base_data.icon_url
|
||||
|
||||
loaded_data = self.app.get_data_to_cache()
|
||||
|
||||
self.api_cache.add(self.app.base_data.id, loaded_data)
|
||||
self.app.status = ApplicationStatus.READY
|
||||
|
||||
if self.app.supports_disk_cache():
|
||||
self.to_persist[self.app.base_data.id] = self.app
|
||||
|
||||
break
|
||||
else:
|
||||
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(
|
||||
self.app.base_data.id, res.status_code, res.content.decode()), Fore.RED)
|
||||
except:
|
||||
self.log_msg("Could not retrieve app data for id '{}'".format(self.app.base_data.id), Fore.YELLOW)
|
||||
traceback.print_exc()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.app.status = ApplicationStatus.READY
|
||||
|
||||
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 = {}
|
||||
|
||||
def clone(self) -> "FlatpakAsyncDataLoader":
|
||||
return FlatpakAsyncDataLoader(manager=self.manager,
|
||||
api_cache=self.api_cache,
|
||||
attempts=self.attempts,
|
||||
http_session=self.http_session,
|
||||
timeout=self.timeout,
|
||||
app=self.app)
|
||||
@@ -2,7 +2,6 @@ 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):
|
||||
@@ -49,6 +48,10 @@ class Application(ABC):
|
||||
def can_be_installed(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def can_be_refreshed(self):
|
||||
return self.installed
|
||||
|
||||
@abstractmethod
|
||||
def get_type(self):
|
||||
pass
|
||||
@@ -60,74 +63,12 @@ class Application(ABC):
|
||||
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()
|
||||
|
||||
@abstractmethod
|
||||
def get_disk_cache_path(self):
|
||||
return '{}/{}'.format(flatpak_cache_path, self.base_data.id)
|
||||
pass
|
||||
|
||||
def get_disk_icon_path(self):
|
||||
return '{}/icon.png'.format(self.get_disk_cache_path())
|
||||
@@ -135,16 +76,13 @@ class FlatpakApplication(Application):
|
||||
def get_disk_data_path(self):
|
||||
return '{}/data.json'.format(self.get_disk_cache_path())
|
||||
|
||||
@abstractmethod
|
||||
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
|
||||
}
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
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])
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return '{} (id={}, name={})'.format(self.__class__.__name__, self.base_data.id, self.base_data.name)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
|
||||
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
from fpakman import ROOT_DIR
|
||||
|
||||
|
||||
def get_path(resource_path):
|
||||
return app_dir + '/resources/' + resource_path
|
||||
return ROOT_DIR + '/resources/' + resource_path
|
||||
|
||||
0
fpakman/core/snap/__init__.py
Normal file
0
fpakman/core/snap/__init__.py
Normal file
4
fpakman/core/snap/constants.py
Normal file
4
fpakman/core/snap/constants.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from fpakman.core.constants import CACHE_PATH
|
||||
|
||||
SNAP_STORE_URL = 'https://snapcraft.io'
|
||||
SNAP_CACHE_PATH = '{}/snap/installed'.format(CACHE_PATH)
|
||||
133
fpakman/core/snap/controller.py
Normal file
133
fpakman/core/snap/controller.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import os
|
||||
import shutil
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from fpakman.core import disk
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.disk import DiskCacheLoader
|
||||
from fpakman.core.model import ApplicationData, Application
|
||||
from fpakman.core.snap import snap
|
||||
from fpakman.core.snap.model import SnapApplication
|
||||
from fpakman.core.snap.worker import SnapAsyncDataLoader
|
||||
from fpakman.core.system import FpakmanProcess
|
||||
from fpakman.util.cache import Cache
|
||||
|
||||
|
||||
class SnapManager(ApplicationManager):
|
||||
|
||||
def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session):
|
||||
super(SnapManager, self).__init__(app_args=app_args)
|
||||
self.api_cache = api_cache
|
||||
self.http_session = http_session
|
||||
self.disk_cache = disk_cache
|
||||
|
||||
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication:
|
||||
app = SnapApplication(publisher=app_json.get('publisher'),
|
||||
rev=app_json.get('rev'),
|
||||
notes=app_json.get('notes'),
|
||||
app_type=app_json.get('type'),
|
||||
base_data=ApplicationData(id=app_json.get('name'),
|
||||
name=app_json.get('name'),
|
||||
version=app_json.get('version'),
|
||||
latest_version=app_json.get('version'),
|
||||
description=app_json.get('description')
|
||||
))
|
||||
|
||||
if app.publisher:
|
||||
app.publisher = app.publisher.replace('*', '')
|
||||
|
||||
app.installed = installed
|
||||
|
||||
api_data = self.api_cache.get(app_json['name'])
|
||||
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
|
||||
|
||||
if (not api_data or expired_data) and not app.is_library():
|
||||
if disk_loader and app.installed:
|
||||
disk_loader.add(app)
|
||||
|
||||
SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session, download_icons=self.app_args.download_icons).start()
|
||||
else:
|
||||
app.fill_cached_data(api_data)
|
||||
|
||||
return app
|
||||
|
||||
def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[SnapApplication]]:
|
||||
installed = self.read_installed(disk_loader)
|
||||
|
||||
res = {'installed': [], 'new': []}
|
||||
|
||||
for app_json in snap.search(word):
|
||||
|
||||
already_installed = None
|
||||
|
||||
if installed:
|
||||
already_installed = [i for i in installed if i.base_data.id == app_json.get('name')]
|
||||
already_installed = already_installed[0] if already_installed else None
|
||||
|
||||
if already_installed:
|
||||
res['installed'].append(already_installed)
|
||||
else:
|
||||
res['new'].append(self.map_json(app_json, installed=False, disk_loader=disk_loader))
|
||||
|
||||
return res
|
||||
|
||||
def read_installed(self, disk_loader: DiskCacheLoader) -> List[SnapApplication]:
|
||||
res = [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()]
|
||||
return res
|
||||
|
||||
def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=snap.downgrade_and_stream(app.base_data.name, root_password), wrong_error_phrase=None)
|
||||
|
||||
def clean_cache_for(self, app: SnapApplication):
|
||||
self.api_cache.delete(app.base_data.name)
|
||||
|
||||
if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()):
|
||||
shutil.rmtree(app.get_disk_cache_path())
|
||||
|
||||
def can_downgrade(self):
|
||||
return True
|
||||
|
||||
def update_and_stream(self, app: SnapApplication) -> FpakmanProcess:
|
||||
pass
|
||||
|
||||
def uninstall_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=snap.uninstall_and_stream(app.base_data.name, root_password))
|
||||
|
||||
def get_app_type(self):
|
||||
return SnapApplication
|
||||
|
||||
def get_info(self, app: SnapApplication) -> dict:
|
||||
info = snap.get_info(app.base_data.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed'))
|
||||
info['description'] = app.base_data.description
|
||||
info['publisher'] = app.publisher
|
||||
info['revision'] = app.rev
|
||||
info['name'] = app.base_data.name
|
||||
|
||||
if info.get('commands'):
|
||||
info['commands'] = ' '.join(info['commands'])
|
||||
|
||||
return info
|
||||
|
||||
def get_history(self, app: Application) -> List[dict]:
|
||||
return []
|
||||
|
||||
def install_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=snap.install_and_stream(app.base_data.name, app.install_cmd, root_password))
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return snap.is_installed()
|
||||
|
||||
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
|
||||
if self.disk_cache and app.supports_disk_cache():
|
||||
disk.save(app, icon_bytes, only_icon)
|
||||
|
||||
def requires_root(self, action: str, app: SnapApplication):
|
||||
return action != 'search'
|
||||
|
||||
def refresh(self, app: SnapApplication, root_password: str) -> FpakmanProcess:
|
||||
return FpakmanProcess(subproc=snap.refresh_and_stream(app.base_data.name, root_password))
|
||||
|
||||
def prepare(self):
|
||||
pass
|
||||
60
fpakman/core/snap/model.py
Normal file
60
fpakman/core/snap/model.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.model import Application, ApplicationData
|
||||
from fpakman.core.snap.constants import SNAP_CACHE_PATH
|
||||
|
||||
|
||||
class SnapApplication(Application):
|
||||
|
||||
def __init__(self, base_data: ApplicationData, publisher: str, rev: str, notes: str, app_type: str, install_cmd: str = None):
|
||||
super(SnapApplication, self).__init__(base_data=base_data)
|
||||
self.publisher = publisher
|
||||
self.rev = rev
|
||||
self.notes = notes
|
||||
self.type = app_type
|
||||
self.install_cmd = install_cmd
|
||||
|
||||
def has_history(self):
|
||||
return False
|
||||
|
||||
def has_info(self):
|
||||
return True
|
||||
|
||||
def can_be_downgraded(self):
|
||||
return self.installed
|
||||
|
||||
def can_be_uninstalled(self):
|
||||
return self.installed
|
||||
|
||||
def can_be_installed(self):
|
||||
return not self.installed
|
||||
|
||||
def can_be_refreshed(self):
|
||||
return self.installed
|
||||
|
||||
def get_type(self):
|
||||
return 'snap'
|
||||
|
||||
def get_default_icon_path(self):
|
||||
return resource.get_path('img/snapcraft.png')
|
||||
|
||||
def is_library(self):
|
||||
return self.type in ('core', 'base', 'snapd') or self.base_data.name.startswith('gtk-') or self.base_data.name.startswith('gnome-')
|
||||
|
||||
def get_disk_cache_path(self):
|
||||
return '{}/{}'.format(SNAP_CACHE_PATH, self.base_data.name)
|
||||
|
||||
def get_data_to_cache(self):
|
||||
return {
|
||||
"icon_url": self.base_data.icon_url,
|
||||
'install_cmd': self.install_cmd,
|
||||
'description': self.base_data.description
|
||||
}
|
||||
|
||||
def fill_cached_data(self, data: dict):
|
||||
if data:
|
||||
for base_attr in ('icon_url', 'description'):
|
||||
if data.get(base_attr):
|
||||
setattr(self.base_data, base_attr, data[base_attr])
|
||||
|
||||
if data.get('install_cmd'):
|
||||
self.install_cmd = data['install_cmd']
|
||||
145
fpakman/core/snap/snap.py
Normal file
145
fpakman/core/snap/snap.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import re
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from fpakman.core import system
|
||||
from fpakman.core.snap.constants import SNAP_STORE_URL
|
||||
|
||||
BASE_CMD = 'snap'
|
||||
|
||||
|
||||
def is_installed():
|
||||
version = get_snapd_version()
|
||||
return False if version is None else True
|
||||
|
||||
|
||||
def get_version():
|
||||
res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False)
|
||||
return res.split('\n')[0].split(' ')[-1].strip() if res else None
|
||||
|
||||
|
||||
def get_snapd_version():
|
||||
res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False)
|
||||
|
||||
if not res:
|
||||
return None
|
||||
else:
|
||||
lines = res.split('\n')
|
||||
|
||||
if lines and len(lines) >= 2:
|
||||
version = lines[1].split(' ')[-1].strip()
|
||||
return version if version and version.lower() != 'unavailable' else None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def app_str_to_json(app: str) -> dict:
|
||||
app_data = [word for word in app.split(' ') if word]
|
||||
app_json = {
|
||||
'name': app_data[0],
|
||||
'version': app_data[1],
|
||||
'rev': app_data[2],
|
||||
'tracking': app_data[3],
|
||||
'publisher': app_data[4],
|
||||
'notes': app_data[5]
|
||||
}
|
||||
|
||||
app_json.update(get_info(app_json['name'], ('summary', 'type', 'description')))
|
||||
return app_json
|
||||
|
||||
|
||||
def get_info(app_name: str, attrs: tuple = None):
|
||||
full_info_lines = system.run_cmd('{} info {}'.format(BASE_CMD, app_name))
|
||||
|
||||
data = {}
|
||||
|
||||
if full_info_lines:
|
||||
re_attrs = r'\w+' if not attrs else '|'.join(attrs)
|
||||
info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines)
|
||||
|
||||
for info in info_map:
|
||||
data[info[0]] = info[1].strip()
|
||||
|
||||
if not attrs or 'description' in attrs:
|
||||
desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines)
|
||||
data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None
|
||||
|
||||
if not attrs or 'commands' in attrs:
|
||||
commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines)
|
||||
data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def read_installed() -> List[dict]:
|
||||
res = system.run_cmd('{} list'.format(BASE_CMD), print_error=False)
|
||||
|
||||
apps = []
|
||||
|
||||
if res and len(res) > 0:
|
||||
lines = res.split('\n')
|
||||
|
||||
if not lines[0].startswith('error'):
|
||||
for idx, app_str in enumerate(lines):
|
||||
if idx > 0 and app_str:
|
||||
apps.append(app_str_to_json(app_str))
|
||||
|
||||
return apps
|
||||
|
||||
|
||||
def search(word: str) -> List[dict]:
|
||||
apps = []
|
||||
|
||||
res = system.run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False)
|
||||
|
||||
if res:
|
||||
res = res.split('\n')
|
||||
|
||||
if not res[0].startswith('No matching'):
|
||||
for idx, app_str in enumerate(res):
|
||||
if idx > 0 and app_str:
|
||||
app_data = [word for word in app_str.split(' ') if word]
|
||||
apps.append({
|
||||
'name': app_data[0],
|
||||
'version': app_data[1],
|
||||
'publisher': app_data[2],
|
||||
'notes': app_data[3] if app_data[3] != '-' else None,
|
||||
'summary': app_data[4] if len(app_data) == 5 else '',
|
||||
'rev': None,
|
||||
'tracking': None,
|
||||
'type': None
|
||||
})
|
||||
|
||||
return apps
|
||||
|
||||
|
||||
def uninstall_and_stream(app_name: str, root_password: str):
|
||||
return system.cmd_as_root([BASE_CMD, 'remove', app_name], root_password)
|
||||
|
||||
|
||||
def install_and_stream(app_name: str, custom_install_cmd: str, root_password: str) -> subprocess.Popen:
|
||||
|
||||
install_cmd = [BASE_CMD, 'install', app_name] # default
|
||||
|
||||
if custom_install_cmd:
|
||||
install_cmd = custom_install_cmd.split(' ')
|
||||
else: # tries to retrieve the snapstore proper installation command
|
||||
res = requests.get('{}/{}'.format(SNAP_STORE_URL, app_name))
|
||||
|
||||
if res.status_code == 200:
|
||||
soup = BeautifulSoup(res.text, 'html.parser')
|
||||
input_install_cmd = soup.find("input", {"id": "snap-install"})
|
||||
install_cmd = input_install_cmd.get("value").strip().split(' ')
|
||||
|
||||
return system.cmd_as_root(install_cmd, root_password)
|
||||
|
||||
|
||||
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
|
||||
return system.cmd_as_root([BASE_CMD, 'revert', app_name], root_password)
|
||||
|
||||
|
||||
def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
|
||||
return system.cmd_as_root([BASE_CMD, 'refresh', app_name], root_password)
|
||||
87
fpakman/core/snap/worker.py
Normal file
87
fpakman/core/snap/worker.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from colorama import Fore
|
||||
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.model import ApplicationStatus
|
||||
from fpakman.core.snap import snap
|
||||
from fpakman.core.snap.constants import SNAP_STORE_URL
|
||||
from fpakman.core.snap.model import SnapApplication
|
||||
from fpakman.core.worker import AsyncDataLoader
|
||||
from fpakman.util.cache import Cache
|
||||
|
||||
|
||||
class SnapAsyncDataLoader(AsyncDataLoader):
|
||||
|
||||
def __init__(self, app: SnapApplication, manager: ApplicationManager, http_session, api_cache: Cache, download_icons: bool, attempts: int = 2, timeout: int = 30):
|
||||
super(SnapAsyncDataLoader, self).__init__(app=app)
|
||||
self.manager = manager
|
||||
self.http_session = http_session
|
||||
self.attempts = attempts
|
||||
self.api_cache = api_cache
|
||||
self.timeout = timeout
|
||||
self.to_persist = {} # stores all data loaded by the instance
|
||||
self.download_icons = download_icons
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
self.app.status = ApplicationStatus.LOADING_DATA
|
||||
if not self.app.base_data.description:
|
||||
self.app.base_data.description = snap.get_info(self.app.base_data.name, ('description',)).get('description')
|
||||
|
||||
for _ in range(0, self.attempts):
|
||||
try:
|
||||
res = self.http_session.get('{}/{}'.format(SNAP_STORE_URL, self.app.base_data.name),
|
||||
timeout=self.timeout)
|
||||
|
||||
if res.status_code == 200 and res.text:
|
||||
soup = BeautifulSoup(res.text, 'html.parser')
|
||||
input_install_cmd = soup.find("input", {"id": "snap-install"})
|
||||
|
||||
api_data = {
|
||||
'install_cmd': input_install_cmd.get("value").strip(),
|
||||
'description': self.app.base_data.description
|
||||
}
|
||||
|
||||
img_icon = None
|
||||
|
||||
if self.download_icons:
|
||||
img_icon = soup.find('img', {"class": "p-snap-heading__icon"})
|
||||
|
||||
if img_icon and img_icon.get("src") and 'snapcraft-missing-icon' not in img_icon.get('src'):
|
||||
api_data['icon_url'] = img_icon.get("src")
|
||||
|
||||
self.api_cache.add(self.app.base_data.id, api_data)
|
||||
self.app.install_cmd = api_data['install_cmd']
|
||||
self.app.base_data.icon_url = api_data.get('icon_url')
|
||||
self.app.status = ApplicationStatus.READY
|
||||
|
||||
if self.app.supports_disk_cache():
|
||||
self.to_persist[self.app.base_data.id] = self.app
|
||||
|
||||
break
|
||||
else:
|
||||
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.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()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.app.status = ApplicationStatus.READY
|
||||
|
||||
def clone(self) -> "SnapAsyncDataLoader":
|
||||
return SnapAsyncDataLoader(manager=self.manager,
|
||||
api_cache=self.api_cache,
|
||||
attempts=self.attempts,
|
||||
http_session=self.http_session,
|
||||
timeout=self.timeout,
|
||||
app=self.app)
|
||||
|
||||
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 = {}
|
||||
@@ -1,12 +0,0 @@
|
||||
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)
|
||||
@@ -2,9 +2,18 @@ import os
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
from fpakman import __app_name__
|
||||
from fpakman.core import resource
|
||||
|
||||
|
||||
class FpakmanProcess:
|
||||
|
||||
def __init__(self, subproc: subprocess.Popen, success_phrase: str = None, wrong_error_phrase: str = '[sudo] password for'):
|
||||
self.subproc = subproc
|
||||
self.success_pgrase = success_phrase
|
||||
self.wrong_error_phrase = wrong_error_phrase
|
||||
|
||||
|
||||
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str:
|
||||
args = {
|
||||
"shell": True,
|
||||
@@ -28,4 +37,15 @@ def cmd_to_subprocess(cmd: List[str]):
|
||||
|
||||
|
||||
def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')):
|
||||
os.system("notify-send {} '{}'".format("-i {}".format(icon_path) if icon_path else '', msg))
|
||||
os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_path) if icon_path else '', msg))
|
||||
|
||||
|
||||
def cmd_as_root(cmd: List[str], root_password: str) -> subprocess.Popen:
|
||||
pwdin, final_cmd = None, []
|
||||
|
||||
if root_password is not None:
|
||||
final_cmd.extend(['sudo', '-S'])
|
||||
pwdin = stream_cmd(['echo', root_password])
|
||||
|
||||
final_cmd.extend(cmd)
|
||||
return subprocess.Popen(final_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
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
|
||||
from fpakman.core.model import Application
|
||||
|
||||
|
||||
class FlatpakAsyncDataLoader(Thread):
|
||||
class AsyncDataLoader(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
|
||||
def __init__(self, app: Application):
|
||||
super(AsyncDataLoader, self).__init__(daemon=True)
|
||||
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
|
||||
self.stop = False
|
||||
self.to_persist = {} # stores all data loaded by the instance
|
||||
self.manager = manager
|
||||
self.app = app
|
||||
|
||||
def log_msg(self, msg: str, color: int = None):
|
||||
final_msg = StringIO()
|
||||
@@ -41,100 +29,3 @@ class FlatpakAsyncDataLoader(Thread):
|
||||
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 = []
|
||||
|
||||
144
fpakman/resources/img/exclamation.svg
Executable file
144
fpakman/resources/img/exclamation.svg
Executable file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 24 24"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="exclamation2.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
|
||||
id="metadata949"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs947"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient955"><stop
|
||||
style="stop-color:#d4aa00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop951" /><stop
|
||||
style="stop-color:#d4aa00;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop953" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient955"
|
||||
id="linearGradient957"
|
||||
x1="-5.0336413"
|
||||
y1="-1.8872991"
|
||||
x2="-22.291842"
|
||||
y2="17.528175"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient955"
|
||||
id="linearGradient1210"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="-5.0336413"
|
||||
y1="-1.8872991"
|
||||
x2="-22.291842"
|
||||
y2="17.528175" /></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="739"
|
||||
id="namedview945"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="3.6611245"
|
||||
inkscape:cy="14.576667"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="260"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
|
||||
<g
|
||||
id="g914">
|
||||
</g>
|
||||
<g
|
||||
id="g916">
|
||||
</g>
|
||||
<g
|
||||
id="g918">
|
||||
</g>
|
||||
<g
|
||||
id="g920">
|
||||
</g>
|
||||
<g
|
||||
id="g922">
|
||||
</g>
|
||||
<g
|
||||
id="g924">
|
||||
</g>
|
||||
<g
|
||||
id="g926">
|
||||
</g>
|
||||
<g
|
||||
id="g928">
|
||||
</g>
|
||||
<g
|
||||
id="g930">
|
||||
</g>
|
||||
<g
|
||||
id="g932">
|
||||
</g>
|
||||
<g
|
||||
id="g934">
|
||||
</g>
|
||||
<g
|
||||
id="g936">
|
||||
</g>
|
||||
<g
|
||||
id="g938">
|
||||
</g>
|
||||
<g
|
||||
id="g940">
|
||||
</g>
|
||||
<g
|
||||
id="g942">
|
||||
</g>
|
||||
<g
|
||||
id="g1217"
|
||||
transform="translate(10.429681,16.353516)"><g
|
||||
transform="translate(-10.429681,-16.353516)"
|
||||
style="fill:url(#linearGradient957);fill-opacity:1"
|
||||
id="g912">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path910"
|
||||
d="M 12,0 C 5.373,0 0,5.373 0,12 0,18.627 5.373,24 12,24 18.627,24 24,18.627 24,12 24,5.373 18.627,0 12,0 Z m 0,19.66 c -0.938,0 -1.58,-0.723 -1.58,-1.66 0,-0.964 0.669,-1.66 1.58,-1.66 0.963,0 1.58,0.696 1.58,1.66 0,0.938 -0.617,1.66 -1.58,1.66 z m 0.622,-6.339 c -0.239,0.815 -0.992,0.829 -1.243,0 -0.289,-0.956 -1.316,-4.585 -1.316,-6.942 0,-3.11 3.891,-3.125 3.891,0 -0.001,2.371 -1.083,6.094 -1.332,6.942 z"
|
||||
style="fill:url(#linearGradient1210);fill-opacity:1" />
|
||||
</g><path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1198"
|
||||
d="m 1.3648508,-2.489113 c -0.250891,-0.124189 -0.327144,-0.271469 -0.55064598,-1.063559 -0.664465,-2.354862 -1.04293,-4.2896531 -1.147198,-5.8647013 -0.0767,-1.1585747 0.09689,-1.7887227 0.643137,-2.3346987 0.4799,-0.479662 1.07478698,-0.648108 1.71268798,-0.48496 0.564701,0.144426 0.973861,0.498107 1.24114,1.072852 0.194111,0.41741 0.235782,0.660945 0.231159,1.3509478 -0.0094,1.4036771 -0.421836,3.6124492 -1.172557,6.2796362 -0.174505,0.619988 -0.250011,0.795473 -0.406612,0.94501 -0.186151,0.177754 -0.337702,0.205108 -0.551111,0.09947 z"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.03125" /><path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1200"
|
||||
d="m 1.5625068,-12.296875 c -0.133701,-4.9e-5 -0.257218,0.01288 -0.375,0.03711 -0.01713,0.0034 -0.04774,0.0019 -0.0625,0.0059 -0.04019,0.01083 -0.07595,0.03503 -0.115235,0.04883 -0.07496,0.02544 -0.14937098,0.05778 -0.22070298,0.0957 -0.0693,0.03572 -0.136178,0.07245 -0.201172,0.117187 -0.395912,0.272524 -0.712194,0.695841 -0.861328,1.210938 -0.06573,0.22717 -0.07016,0.309243 -0.06836,0.9374996 2.45e-4,0.086621 0.01357,0.1776107 0.01563,0.2636719 0.0078,0.2791513 0.0217,0.567286 0.05469,0.8886719 0.02567,0.2619407 0.07197,0.5785227 0.117188,0.8730468 0.02898,0.1904904 0.0596,0.3838721 0.0957,0.5859376 0.02975,0.1646911 0.04058,0.2865674 0.07617,0.4667968 0.02253,0.1140684 0.06671,0.2792068 0.09375,0.40625 0.08083,0.3833354 0.176637,0.7912044 0.279296,1.2089844 0.246077,1.01458 0.501767,1.987896 0.61914098,2.261719 0.03892,0.0908 0.09578,0.1787 0.160156,0.253906 0.0136,0.01632 0.0326,0.02725 0.04687,0.04102 0.04125,0.0418 0.08219,0.08717 0.125,0.113281 0.06319,0.03853 0.132973,0.05138 0.205078,0.05273 0.05796,0.0011 0.116036,-0.008 0.173829,-0.0293 0.0075,-0.0027 0.01406,-0.0086 0.02148,-0.01172 0.05163,-0.02164 0.101553,-0.04945 0.148437,-0.08789 0.0063,-0.0052 0.01142,-0.01207 0.01758,-0.01758 0.03276,-0.02895 0.06728,-0.05485 0.0957,-0.0918 0.0047,-0.0061 0.01259,-0.03336 0.01758,-0.04102 0.0052,-0.0076 0.01256,-0.01166 0.01758,-0.01953 0.0081,-0.01277 0.02602,-0.07759 0.03516,-0.0957 0.06526,-0.134622 0.143353,-0.353611 0.236328,-0.652344 0.0061,-0.02016 0.01142,-0.03393 0.01758,-0.05469 0.08547,-0.278648 0.179338,-0.659826 0.277344,-1.03711 0.07187,-0.280521 0.135865,-0.545447 0.207032,-0.847656 0.07372,-0.310588 0.144002,-0.546795 0.216796,-0.882812 0.07915,-0.3653757 0.116018,-0.6348283 0.173828,-0.9511722 0.04874,-0.2610293 0.108943,-0.5530308 0.140626,-0.7675782 10e-4,-0.00687 9.46e-4,-0.012662 0.002,-0.019531 0.07245,-0.4988018 0.120186,-0.9630243 0.136718,-1.3847656 0.004,-0.069734 0.01867,-0.1529162 0.02149,-0.2207032 0.01305,-0.3147671 -0.006,-0.5821898 -0.04102,-0.8300778 -0.0308,-0.279799 -0.08807,-0.50458 -0.222656,-0.773438 -0.01126,-0.02248 -0.0271,-0.0406 -0.03906,-0.0625 -0.02477,-0.04414 -0.05401,-0.08682 -0.08203,-0.128906 -0.06991,-0.10808 -0.147597,-0.206648 -0.234375,-0.296875 -0.0099,-0.01034 -0.01524,-0.02297 -0.02539,-0.0332 -0.0097,-0.0098 -0.02143,-0.01589 -0.03125,-0.02539 -0.06451,-0.06161 -0.13129,-0.118543 -0.203124,-0.169922 -0.03372,-0.02443 -0.06852,-0.0466 -0.103516,-0.06836 -0.06898,-0.04263 -0.138762,-0.08215 -0.212891,-0.115234 -0.03375,-0.01482 -0.06876,-0.02643 -0.103515,-0.03906 -0.05821,-0.02181 -0.113151,-0.0506 -0.173828,-0.06641 -0.01977,-0.0052 -0.057,-0.0035 -0.08008,-0.0078 -0.124433,-0.02492 -0.253549,-0.03901 -0.388671,-0.03906 z"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.03125" /><path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1205"
|
||||
d="m 1.5800848,0.015625 c -0.0178,0 -0.03152,0.0036 -0.04883,0.0039 -0.08654,0.002 -0.170637,0.0024 -0.25586,0.01758 -0.13064,0.01899 -0.248148,0.05582 -0.35742198,0.105469 -0.01495,0.0066 -0.03215,0.0066 -0.04687,0.01367 -0.01917,0.0092 -0.03902,0.03191 -0.05859,0.04297 -0.08499,0.04925 -0.168809,0.110204 -0.25,0.183594 -0.07098,0.06223 -0.123346,0.120203 -0.183594,0.191406 -0.04461,0.05354 -0.08918,0.107632 -0.125,0.164063 -0.01932,0.0304 -0.05333,0.06019 -0.06836,0.08984 -0.02046,0.04038 -0.03184,0.09021 -0.04883,0.134765 -0.007,0.01713 -0.01317,0.03525 -0.01953,0.05273 -0.02632,0.07746 -0.04829,0.155268 -0.06445,0.240234 -0.0059,0.03085 -0.0093,0.06368 -0.01367,0.0957 -0.0137,0.09626 -0.02276,0.191001 -0.02344,0.289063 -1.3e-5,0.0034 0,0.0063 0,0.0098 0,0.0038 0.0019,0.0078 0.002,0.01172 4.07e-4,0.136442 0.01441,0.266964 0.04101,0.392578 0.0035,0.01807 0.0021,0.04995 0.0059,0.06445 0.0034,0.01335 0.01187,0.02392 0.01563,0.03711 0.0062,0.0218 0.0066,0.04736 0.01367,0.06836 0.02672,0.07959 0.06612,0.147112 0.101562,0.21875 0.01461,0.02886 0.02478,0.06019 0.04102,0.08789 0.202316,0.355898 0.515811,0.606046 0.92578098,0.71875 0.114715,0.03154 0.285952,0.03886 0.457031,0.03516 0.03918,-0.0012 0.07647,-0.0058 0.115234,-0.0098 0.06575,-0.005 0.140201,-0.0026 0.19336,-0.01367 0.06811,-0.0142 0.134222,-0.04299 0.201172,-0.06836 0.333189,-0.126241 0.641516,-0.374039 0.800781,-0.669922 0.02144,-0.03983 0.0249,-0.08731 0.04297,-0.128906 0.0434,-0.09308 0.07854,-0.19749 0.103515,-0.304688 0.01416,-0.06169 0.02721,-0.12056 0.03516,-0.183593 0.02001,-0.144005 0.02205,-0.286726 0.0098,-0.427735 -0.0047,-0.05246 -0.01464,-0.100691 -0.02344,-0.152343 -0.0201,-0.117318 -0.05021,-0.229314 -0.0918,-0.337891 -0.02117,-0.05719 -0.03975,-0.112112 -0.06641,-0.166016 -0.04827,-0.0948 -0.11061,-0.177917 -0.175782,-0.259765 -0.03204,-0.04093 -0.04978,-0.09412 -0.08594,-0.13086 -0.02601,-0.02643 -0.06342,-0.04035 -0.0918,-0.06445 -0.08406,-0.07533 -0.172768,-0.138308 -0.265625,-0.1875 -0.01534,-0.0078 -0.02927,-0.01619 -0.04492,-0.02344 -0.118257,-0.0564 -0.246131,-0.09834 -0.390625,-0.119141 -0.08008,-0.01479 -0.160229,-0.01418 -0.242187,-0.01758 -0.02205,-5.53e-4 -0.03973,-0.0039 -0.0625,-0.0039 z"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.03125" /></g></svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
BIN
fpakman/resources/img/snapcraft.png
Executable file
BIN
fpakman/resources/img/snapcraft.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -9,7 +9,8 @@ manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} ?
|
||||
manage_window.apps_table.row.actions.downgrade=Downgrade
|
||||
manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ?
|
||||
manage_window.apps_table.row.actions.install=Install
|
||||
manage_window.apps_table.upgrade_toggle.tooltip=Click here to check or uncheck the update
|
||||
manage_window.apps_table.row.actions.refresh=Refresh
|
||||
manage_window.apps_table.upgrade_toggle.tooltip=There is an update for this application. Click here to check or uncheck the update
|
||||
manage_window.checkbox.only_apps=Apps
|
||||
window_manage.input_search.placeholder=Search
|
||||
window_manage.input_search.tooltip=Type and press ENTER to search for applications
|
||||
@@ -23,7 +24,7 @@ manage_window.status.history=Retrieving history
|
||||
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
|
||||
manage_window.bt.upgrade.tooltip=Upgrade all checked applications
|
||||
manage_window.checkbox.show_details=Show details
|
||||
popup.root.title=Requires root privileges
|
||||
popup.root.password=Password
|
||||
@@ -41,6 +42,11 @@ tray.action.exit=Exit
|
||||
tray.action.about=About
|
||||
tray.action.refreshing=Refreshing
|
||||
notification.new_updates=Updates
|
||||
notification.update_selected.success=app(s) updated successfully
|
||||
notification.update_selected.failed=Update failed
|
||||
notification.install.failed=installation failed
|
||||
notification.uninstall.failed=uninstallation failed
|
||||
notification.downgrade.failed=Failed to downgrade
|
||||
flatpak.info.arch=arch
|
||||
flatpak.info.branch=branch
|
||||
flatpak.info.collection=collection
|
||||
@@ -60,7 +66,15 @@ flatpak.info.sdk=sdk
|
||||
flatpak.info.subject=subject
|
||||
flatpak.info.type=type
|
||||
flatpak.info.version=version
|
||||
about.info.desc=Non-official Flatpak application management graphical interface
|
||||
snap.info.commands=commands
|
||||
snap.info.contact=contact
|
||||
snap.info.description=description
|
||||
snap.info.license=license
|
||||
snap.info.revision=revision
|
||||
snap.info.tracking=tracking
|
||||
snap.info.installed=installed
|
||||
snap.info.publisher=publisher
|
||||
about.info.desc=Non-official Flatpak / Snap application management graphical interface
|
||||
about.info.link=More information at
|
||||
about.info.license=Free license
|
||||
about.info.rate=If this tool is useful for you, give it a star on Github to keep it up
|
||||
@@ -74,5 +88,14 @@ latest_version=latest version
|
||||
description=description
|
||||
type=type
|
||||
installed=installed
|
||||
uninstalled=not installed
|
||||
downgraded=downgraded
|
||||
others=others
|
||||
internet.required=Internet connection is required
|
||||
license.unset=unset
|
||||
updates=updates
|
||||
version.installed=installed version
|
||||
version.latest=latest version
|
||||
version.installed_outdated=the installed version is outdated
|
||||
version.unknown=not informed
|
||||
app.name=application name
|
||||
@@ -10,6 +10,7 @@ manage_window.apps_table.row.actions.uninstall.popup.body=¿Eliminar {}?
|
||||
manage_window.apps_table.row.actions.downgrade=Revertir versión
|
||||
manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quieres revertir la versión actual de {}?
|
||||
manage_window.apps_table.row.actions.install=Instalar
|
||||
manage_window.apps_table.row.actions.refresh=Actualizar
|
||||
manage_window.apps_table.upgrade_toggle.tooltip=Haces clic aqui para marcar o desmarcar la actualización
|
||||
manage_window.checkbox.only_apps=Aplicativos
|
||||
window_manage.input_search.placeholder=Buscar
|
||||
@@ -24,7 +25,7 @@ manage_window.status.history=Obteniendo la historia
|
||||
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
|
||||
manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos marcados
|
||||
manage_window.checkbox.show_details=Mostrar detalles
|
||||
popup.root.title=Requiere privilegios de root
|
||||
popup.root.password=Contraseña
|
||||
@@ -42,6 +43,11 @@ tray.action.exit=Salir
|
||||
tray.action.about=Sobre
|
||||
tray.action.refreshing=Recargando
|
||||
notification.new_updates=Actualizaciones
|
||||
notification.update_selected.success=aplicativo(s) actualizado(s) con éxito
|
||||
notification.update_selected.failed=La actualización falló
|
||||
notification.install.failed=la instalación ha fallado
|
||||
notification.uninstall.failed=la desinstalación ha fallado
|
||||
notification.downgrade.failed=Error al revertir la versión
|
||||
flatpak.info.arch=arquitectura
|
||||
flatpak.info.branch=rama
|
||||
flatpak.info.collection=colección
|
||||
@@ -61,7 +67,15 @@ flatpak.info.sdk=sdk
|
||||
flatpak.info.subject=tema
|
||||
flatpak.info.type=tipo
|
||||
flatpak.info.version=versión
|
||||
about.info.desc=Interfaz grafica no oficial para administración de aplicativos Flatpak
|
||||
snap.info.commands=comandos
|
||||
snap.info.contact=contacto
|
||||
snap.info.description=descripción
|
||||
snap.info.license=licencia
|
||||
snap.info.revision=revisión
|
||||
snap.info.tracking=tracking
|
||||
snap.info.installed=instalado
|
||||
snap.info.publisher=publicador
|
||||
about.info.desc=Interfaz grafica no oficial para administración de aplicativos Flatpak / Snap
|
||||
about.info.link=Mas informaciones en
|
||||
about.info.license=Licencia gratuita
|
||||
about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla
|
||||
@@ -75,5 +89,14 @@ latest_version=ultima versión
|
||||
description=descripción
|
||||
type=tipo
|
||||
installed=instalado
|
||||
uninstalled=no instalado
|
||||
downgraded=versión revertida
|
||||
others=otros
|
||||
internet.required=Se requiere conexión a internet
|
||||
license.unset=No está definida
|
||||
updates=actualizaciones
|
||||
version.installed=versión instalada
|
||||
version.latest=versión más reciente
|
||||
version.installed_outdated=la versión instalada está desactualizada
|
||||
version.unknown=versión no informada
|
||||
app.name=nombre del aplicativo
|
||||
@@ -10,7 +10,8 @@ manage_window.apps_table.row.actions.uninstall.popup.body=Remover {}?
|
||||
manage_window.apps_table.row.actions.downgrade=Reverter versão
|
||||
manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ?
|
||||
manage_window.apps_table.row.actions.install=Instalar
|
||||
manage_window.apps_table.upgrade_toggle.tooltip=Clique aqui para marcar ou desmarcar a atualização
|
||||
manage_window.apps_table.row.actions.refresh=Atualizar
|
||||
manage_window.apps_table.upgrade_toggle.tooltip=Existe um atualização para esse aplicativo. Clique aqui para marcar ou desmarcar a atualização
|
||||
manage_window.checkbox.only_apps=Aplicativos
|
||||
window_manage.input_search.placeholder=Buscar
|
||||
window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos
|
||||
@@ -24,7 +25,7 @@ manage_window.status.history=Obtendo histórico
|
||||
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
|
||||
manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos marcados
|
||||
manage_window.checkbox.show_details=Mostrar detalhes
|
||||
popup.root.title=Requer privilégios de root
|
||||
popup.root.password=Senha
|
||||
@@ -42,6 +43,11 @@ tray.action.exit=Sair
|
||||
tray.action.about=Sobre
|
||||
tray.action.refreshing=Recarregando
|
||||
notification.new_updates=Atualizações
|
||||
notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso
|
||||
notification.update_selected.failed=Erro ao atualizar
|
||||
notification.install.failed=instalação falhou
|
||||
notification.uninstall.failed=desinstalação falhou
|
||||
notification.downgrade.failed=Erro ao reverter versão
|
||||
flatpak.info.arch=arquitetura
|
||||
flatpak.info.branch=ramo
|
||||
flatpak.info.collection=coleção
|
||||
@@ -61,7 +67,15 @@ flatpak.info.sdk=sdk
|
||||
flatpak.info.subject=assunto
|
||||
flatpak.info.type=tipo
|
||||
flatpak.info.version=versão
|
||||
about.info.desc=Interface gráfica não oficial para gerenciamento de aplicativos Flatpak
|
||||
snap.info.commands=comandos
|
||||
snap.info.contact=contato
|
||||
snap.info.description=descrição
|
||||
snap.info.license=licença
|
||||
snap.info.revision=revisão
|
||||
snap.info.tracking=tracking
|
||||
snap.info.installed=instalado
|
||||
snap.info.publisher=publicador
|
||||
about.info.desc=Interface gráfica não oficial para gerenciamento de aplicativos Flatpak / Snap
|
||||
about.info.link=Mais informações em
|
||||
about.info.license=Licença gratuita
|
||||
about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé
|
||||
@@ -75,5 +89,14 @@ latest_version=última versão
|
||||
description=descrição
|
||||
type=tipo
|
||||
installed=instalado
|
||||
uninstalled=não instalado
|
||||
downgraded=versão revertida
|
||||
others=outros
|
||||
internet.required=É necessário estar conectado a Internet
|
||||
license.unset=Não definida
|
||||
updates=atualizações
|
||||
version.installed=versão instalada
|
||||
version.latest=versão mais recente
|
||||
version.installed_outdated=a versão instalada está desatualizada
|
||||
version.unknown=versão não informada
|
||||
app.name=nome do aplicativo
|
||||
@@ -19,7 +19,8 @@ class Cache:
|
||||
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}
|
||||
if key:
|
||||
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):
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ from threading import Lock
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor
|
||||
from PyQt5.QtGui import QPixmap, QIcon, QCursor
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
|
||||
QHeaderView, QLabel
|
||||
QHeaderView, QLabel, QHBoxLayout
|
||||
|
||||
from fpakman.core import resource
|
||||
from fpakman.core.model import ApplicationStatus
|
||||
@@ -26,7 +26,7 @@ class UpdateToggleButton(QToolButton):
|
||||
self.icon_on = QIcon(resource.get_path('img/toggle_on.svg'))
|
||||
self.icon_off = QIcon(resource.get_path('img/toggle_off.svg'))
|
||||
self.setIcon(self.icon_on)
|
||||
self.setStyleSheet('border: 0px;')
|
||||
self.setStyleSheet('QToolButton { border: 0px; }')
|
||||
self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip'])
|
||||
|
||||
if not checked:
|
||||
@@ -40,15 +40,15 @@ class UpdateToggleButton(QToolButton):
|
||||
|
||||
class AppsTable(QTableWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool):
|
||||
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool, download_icons: bool):
|
||||
super(AppsTable, self).__init__()
|
||||
self.setParent(parent)
|
||||
self.window = parent
|
||||
self.disk_cache = disk_cache
|
||||
self.download_icons = download_icons
|
||||
self.column_names = [parent.locale_keys[key].capitalize() for key in ['name',
|
||||
'version',
|
||||
'latest_version',
|
||||
'flatpak.info.description',
|
||||
'description',
|
||||
'type',
|
||||
'installed',
|
||||
'manage_window.columns.update']]
|
||||
@@ -56,6 +56,7 @@ class AppsTable(QTableWidget):
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
self.setShowGrid(False)
|
||||
self.verticalHeader().setVisible(False)
|
||||
self.horizontalHeader().setVisible(False)
|
||||
self.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.setHorizontalHeaderLabels(self.column_names)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
@@ -74,13 +75,19 @@ class AppsTable(QTableWidget):
|
||||
|
||||
menu_row = QMenu()
|
||||
|
||||
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)
|
||||
|
||||
if app.model.installed:
|
||||
|
||||
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)
|
||||
if app.model.can_be_refreshed():
|
||||
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.refresh"])
|
||||
action_history.setIcon(QIcon(resource.get_path('img/refresh.svg')))
|
||||
action_history.triggered.connect(self._refresh_app)
|
||||
menu_row.addAction(action_history)
|
||||
|
||||
if app.model.has_history():
|
||||
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
|
||||
@@ -99,6 +106,7 @@ class AppsTable(QTableWidget):
|
||||
action_downgrade.triggered.connect(self._downgrade_app)
|
||||
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
|
||||
menu_row.addAction(action_downgrade)
|
||||
|
||||
else:
|
||||
if app.model.can_be_installed():
|
||||
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
|
||||
@@ -119,15 +127,17 @@ class AppsTable(QTableWidget):
|
||||
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)))
|
||||
|
||||
if self.download_icons:
|
||||
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)
|
||||
self._set_col_version(idx, app_v)
|
||||
self._set_col_description(idx, app_v)
|
||||
app_v.status = ApplicationViewStatus.READY
|
||||
|
||||
self.window.resize_and_center()
|
||||
@@ -156,6 +166,9 @@ class AppsTable(QTableWidget):
|
||||
locale_keys=self.window.locale_keys):
|
||||
self.window.downgrade_app(selected_app)
|
||||
|
||||
def _refresh_app(self):
|
||||
self.window.refresh(self.get_selected_app())
|
||||
|
||||
def _get_app_info(self):
|
||||
self.window.get_app_info(self.get_selected_app())
|
||||
|
||||
@@ -195,43 +208,67 @@ class AppsTable(QTableWidget):
|
||||
|
||||
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 = QLabel(app_v.model.base_data.version)
|
||||
col_version.setAlignment(Qt.AlignCenter)
|
||||
self.setCellWidget(idx, 1, col_version)
|
||||
|
||||
col_release = QLabel(app_v.get_async_attr('latest_version'))
|
||||
col_release.setAlignment(Qt.AlignCenter)
|
||||
self.setCellWidget(idx, 2, col_release)
|
||||
|
||||
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_description = QTableWidgetItem()
|
||||
col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
self._set_col_description(col_description, app_v)
|
||||
|
||||
self.setItem(idx, 3, col_description)
|
||||
|
||||
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_v.model.installed else 'red_cross')))))
|
||||
col_installed.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.setCellWidget(idx, 5, col_installed)
|
||||
self._set_col_name(idx, app_v)
|
||||
self._set_col_version(idx, app_v)
|
||||
self._set_col_description(idx, app_v)
|
||||
self._set_col_type(idx, app_v)
|
||||
self._set_col_installed(idx, app_v)
|
||||
|
||||
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)
|
||||
self.setCellWidget(idx, 5, col_update)
|
||||
|
||||
def _gen_col_name(self, app_v: ApplicationView):
|
||||
def _set_col_installed(self, idx: int, app_v: ApplicationView):
|
||||
col_installed = QLabel()
|
||||
|
||||
if app_v.model.installed:
|
||||
img_name = 'checked'
|
||||
tooltip = self.window.locale_keys['installed']
|
||||
else:
|
||||
img_name = 'red_cross'
|
||||
tooltip = self.window.locale_keys['uninstalled']
|
||||
|
||||
col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format(img_name)))))
|
||||
col_installed.setAlignment(Qt.AlignCenter)
|
||||
col_installed.setToolTip(tooltip)
|
||||
|
||||
self.setCellWidget(idx, 4, col_installed)
|
||||
|
||||
def _set_col_type(self, idx: int, app_v: ApplicationView):
|
||||
col_type = QLabel()
|
||||
pixmap = QPixmap(app_v.model.get_default_icon_path())
|
||||
col_type.setPixmap(pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
col_type.setAlignment(Qt.AlignCenter)
|
||||
col_type.setToolTip('{}: {}'.format(self.window.locale_keys['type'], app_v.model.get_type()))
|
||||
self.setCellWidget(idx, 3, col_type)
|
||||
|
||||
def _set_col_version(self, idx: int, app_v: ApplicationView):
|
||||
label_version = QLabel(app_v.model.base_data.version if app_v.model.base_data.version else '?')
|
||||
label_version.setAlignment(Qt.AlignCenter)
|
||||
|
||||
col_version = QWidget()
|
||||
col_version.setLayout(QHBoxLayout())
|
||||
col_version.layout().addWidget(label_version)
|
||||
|
||||
if app_v.model.base_data.version:
|
||||
tooltip = self.window.locale_keys['version.installed'] if app_v.model.installed else self.window.locale_keys['version']
|
||||
else:
|
||||
tooltip = self.window.locale_keys['version.unknown']
|
||||
|
||||
if app_v.model.update:
|
||||
label_version.setStyleSheet("color: #32CD32")
|
||||
tooltip = self.window.locale_keys['version.installed_outdated']
|
||||
|
||||
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:
|
||||
tooltip = '{}. {}: {}'.format(tooltip, self.window.locale_keys['version.latest'], app_v.model.base_data.latest_version)
|
||||
|
||||
col_version.setToolTip(tooltip)
|
||||
self.setCellWidget(idx, 1, col_version)
|
||||
|
||||
def _set_col_name(self, idx: int, 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)
|
||||
col.setToolTip(self.window.locale_keys['app.name'].lower())
|
||||
|
||||
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:
|
||||
@@ -240,6 +277,7 @@ class AppsTable(QTableWidget):
|
||||
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:
|
||||
@@ -247,9 +285,11 @@ class AppsTable(QTableWidget):
|
||||
icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path())
|
||||
|
||||
col.setIcon(icon)
|
||||
return col
|
||||
self.setItem(idx, 0, col)
|
||||
|
||||
def _set_col_description(self, col: QTableWidgetItem, app_v: ApplicationView):
|
||||
def _set_col_description(self, idx: int, app_v: ApplicationView):
|
||||
col = QTableWidgetItem()
|
||||
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
desc = app_v.get_async_attr('description', strip_html=True)
|
||||
|
||||
if desc:
|
||||
@@ -258,6 +298,8 @@ class AppsTable(QTableWidget):
|
||||
if app_v.model.status == ApplicationStatus.READY:
|
||||
col.setToolTip(desc)
|
||||
|
||||
self.setItem(idx, 2, col)
|
||||
|
||||
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
|
||||
header_horizontal = self.horizontalHeader()
|
||||
for i in range(self.columnCount()):
|
||||
|
||||
@@ -2,7 +2,7 @@ import operator
|
||||
from functools import reduce
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtGui import QIcon, QColor
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class HistoryDialog(QDialog):
|
||||
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
|
||||
|
||||
if current_app_commit:
|
||||
item.setBackground(Qt.darkYellow if row != 0 else Qt.darkGreen)
|
||||
item.setBackground(QColor('#ffbf00' if row != 0 else '#32CD32'))
|
||||
tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize())
|
||||
|
||||
item.setToolTip(tip)
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \
|
||||
QLineEdit, QLabel, QPlainTextEdit
|
||||
QLineEdit, QLabel
|
||||
|
||||
from fpakman.util import util
|
||||
|
||||
|
||||
class InfoDialog(QDialog):
|
||||
|
||||
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict):
|
||||
def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict, app_type: str, screen_size: QSize()):
|
||||
super(InfoDialog, self).__init__()
|
||||
self.setWindowTitle(app['name'])
|
||||
self.setWindowIcon(app_icon)
|
||||
self.screen_size = screen_size
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
gbox_info_layout = QFormLayout()
|
||||
gbox_info = QGroupBox()
|
||||
gbox_info.setMaximumHeight(self.screen_size.height() - self.screen_size.height() * 0.1)
|
||||
gbox_info_layout = QFormLayout()
|
||||
gbox_info.setLayout(gbox_info_layout)
|
||||
|
||||
layout.addWidget(gbox_info)
|
||||
|
||||
for attr in sorted(app.keys()):
|
||||
|
||||
if attr != 'name' and app[attr]:
|
||||
if attr == 'description':
|
||||
text = QPlainTextEdit()
|
||||
text.appendHtml(app[attr])
|
||||
else:
|
||||
text = QLineEdit()
|
||||
text.setText(app[attr])
|
||||
text.setCursorPosition(0)
|
||||
text.setStyleSheet("width: 400px")
|
||||
val = app[attr]
|
||||
text = QLineEdit()
|
||||
text.setToolTip(val)
|
||||
|
||||
if attr == 'license' and val.strip() == 'unset':
|
||||
val = locale_keys['license.unset']
|
||||
|
||||
if attr == 'description':
|
||||
val = util.strip_html(val)
|
||||
val = val[0:40] + '...'
|
||||
|
||||
text.setText(val)
|
||||
text.setCursorPosition(0)
|
||||
text.setStyleSheet("width: 400px")
|
||||
text.setReadOnly(True)
|
||||
|
||||
label = QLabel("{}: ".format(locale_keys.get('flatpak.info.' + attr, attr)).capitalize())
|
||||
label = QLabel("{}: ".format(locale_keys.get(app_type + '.info.' + attr, attr)).capitalize())
|
||||
label.setStyleSheet("font-weight: bold")
|
||||
|
||||
gbox_info_layout.addRow(label, text)
|
||||
|
||||
@@ -2,14 +2,15 @@ import time
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt, QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
|
||||
|
||||
from fpakman.core import resource, system
|
||||
from fpakman.core.controller import FlatpakManager, ApplicationManager
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.model import Application
|
||||
from fpakman.util.cache import Cache
|
||||
from fpakman.view.qt import dialog
|
||||
from fpakman.view.qt.about import AboutDialog
|
||||
from fpakman.view.qt.window import ManageWindow
|
||||
|
||||
@@ -39,12 +40,14 @@ class UpdateCheck(QThread):
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
|
||||
def __init__(self, locale_keys: dict, manager: ApplicationManager, icon_cache: Cache, disk_cache: bool, check_interval: int = 60, update_notification: bool = True):
|
||||
def __init__(self, locale_keys: dict, manager: ApplicationManager, icon_cache: Cache, disk_cache: bool, download_icons: bool, screen_size: QSize, 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.download_icons = download_icons
|
||||
self.screen_size = screen_size
|
||||
|
||||
self.icon_default = QIcon(resource.get_path('img/logo.png'))
|
||||
self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
|
||||
@@ -74,6 +77,12 @@ class TrayIcon(QSystemTrayIcon):
|
||||
self.update_notification = update_notification
|
||||
self.lock_notify = Lock()
|
||||
|
||||
self.activated.connect(self.handle_click)
|
||||
|
||||
def handle_click(self, reason):
|
||||
if reason == self.Trigger:
|
||||
self.show_manage_window()
|
||||
|
||||
def notify_updates(self, updates: List[Application]):
|
||||
|
||||
self.lock_notify.acquire()
|
||||
@@ -110,12 +119,14 @@ class TrayIcon(QSystemTrayIcon):
|
||||
manager=self.manager,
|
||||
icon_cache=self.icon_cache,
|
||||
tray_icon=self,
|
||||
disk_cache=self.disk_cache)
|
||||
disk_cache=self.disk_cache,
|
||||
download_icons=self.download_icons,
|
||||
screen_size=self.screen_size)
|
||||
|
||||
if self.manage_window.isMinimized():
|
||||
self.manage_window.setWindowState(Qt.WindowNoState)
|
||||
else:
|
||||
self.manage_window.refresh()
|
||||
elif not self.manage_window.isVisible():
|
||||
self.manage_window.refresh_apps()
|
||||
self.manage_window.show()
|
||||
|
||||
def show_about(self):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
@@ -8,14 +9,51 @@ from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.exception import NoInternetException
|
||||
from fpakman.core.model import ApplicationStatus
|
||||
from fpakman.core.system import FpakmanProcess
|
||||
from fpakman.util.cache import Cache
|
||||
from fpakman.view.qt import dialog
|
||||
from fpakman.view.qt.view_model import ApplicationView
|
||||
|
||||
|
||||
class UpdateSelectedApps(QThread):
|
||||
class AsyncAction(QThread):
|
||||
|
||||
signal_finished = pyqtSignal(bool)
|
||||
def notify_subproc_outputs(self, proc: FpakmanProcess, signal) -> bool:
|
||||
"""
|
||||
:param subproc:
|
||||
:param signal:
|
||||
:param success:
|
||||
:return: if the subprocess succeeded
|
||||
"""
|
||||
signal.emit(' '.join(proc.subproc.args) + '\n')
|
||||
|
||||
success, already_succeeded = True, False
|
||||
|
||||
for output in proc.subproc.stdout:
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
signal.emit(line)
|
||||
|
||||
if proc.success_pgrase and proc.success_pgrase in line:
|
||||
already_succeeded = True
|
||||
|
||||
if already_succeeded:
|
||||
return True
|
||||
|
||||
for output in proc.subproc.stderr:
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
if proc.wrong_error_phrase and proc.wrong_error_phrase in line:
|
||||
continue
|
||||
else:
|
||||
success = False
|
||||
signal.emit(line)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
class UpdateSelectedApps(AsyncAction):
|
||||
|
||||
signal_finished = pyqtSignal(bool, int)
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
|
||||
@@ -25,31 +63,20 @@ class UpdateSelectedApps(QThread):
|
||||
|
||||
def run(self):
|
||||
|
||||
error = False
|
||||
success = False
|
||||
|
||||
for app in self.apps_to_update:
|
||||
|
||||
subproc = self.manager.update_and_stream(app.model)
|
||||
process = self.manager.update_and_stream(app.model)
|
||||
success = self.notify_subproc_outputs(process, self.signal_output)
|
||||
|
||||
self.signal_output.emit(' '.join(subproc.args) + '\n')
|
||||
|
||||
for output in subproc.stdout:
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
|
||||
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:
|
||||
if not success:
|
||||
break
|
||||
else:
|
||||
self.signal_output.emit('\n')
|
||||
|
||||
self.signal_finished.emit(not error)
|
||||
self.signal_finished.emit(success, len(self.apps_to_update))
|
||||
self.apps_to_update = None
|
||||
|
||||
|
||||
class RefreshApps(QThread):
|
||||
@@ -64,8 +91,8 @@ class RefreshApps(QThread):
|
||||
self.signal.emit(self.manager.read_installed())
|
||||
|
||||
|
||||
class UninstallApp(QThread):
|
||||
signal_finished = pyqtSignal()
|
||||
class UninstallApp(AsyncAction):
|
||||
signal_finished = pyqtSignal(bool)
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
|
||||
@@ -73,33 +100,23 @@ class UninstallApp(QThread):
|
||||
self.app = app
|
||||
self.manager = manager
|
||||
self.icon_cache = icon_cache
|
||||
self.root_password = None
|
||||
|
||||
def run(self):
|
||||
if self.app:
|
||||
subproc = self.manager.uninstall_and_stream(self.app.model)
|
||||
self.signal_output.emit(' '.join(subproc.args) + '\n')
|
||||
process = self.manager.uninstall_and_stream(self.app.model, self.root_password)
|
||||
success = self.notify_subproc_outputs(process, self.signal_output)
|
||||
|
||||
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:
|
||||
if success:
|
||||
self.icon_cache.delete(self.app.model.base_data.icon_url)
|
||||
self.manager.clean_cache_for(self.app.model)
|
||||
|
||||
self.signal_finished.emit()
|
||||
self.signal_finished.emit(success)
|
||||
self.app = None
|
||||
self.root_password = None
|
||||
|
||||
|
||||
class DowngradeApp(QThread):
|
||||
class DowngradeApp(AsyncAction):
|
||||
signal_finished = pyqtSignal(bool)
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
@@ -113,18 +130,15 @@ class DowngradeApp(QThread):
|
||||
def run(self):
|
||||
if self.app:
|
||||
|
||||
success = True
|
||||
success = False
|
||||
try:
|
||||
stream = self.manager.downgrade_app(self.app.model, self.root_password)
|
||||
process = self.manager.downgrade_app(self.app.model, self.root_password)
|
||||
|
||||
if stream is None:
|
||||
if process 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)
|
||||
success = self.notify_subproc_outputs(process, self.signal_output)
|
||||
except (requests.exceptions.ConnectionError, NoInternetException):
|
||||
success = False
|
||||
self.signal_output.emit(self.locale_keys['internet.required'])
|
||||
@@ -180,54 +194,51 @@ class SearchApps(QThread):
|
||||
apps_found = []
|
||||
|
||||
if self.word:
|
||||
apps_found = self.manager.search(self.word)
|
||||
res = self.manager.search(self.word)
|
||||
apps_found.extend(res['installed'])
|
||||
apps_found.extend(res['new'])
|
||||
|
||||
self.signal_finished.emit(apps_found)
|
||||
self.word = None
|
||||
|
||||
|
||||
class InstallApp(QThread):
|
||||
class InstallApp(AsyncAction):
|
||||
|
||||
signal_finished = pyqtSignal(bool)
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, app: ApplicationView = None):
|
||||
def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, locale_keys: dict, app: ApplicationView = None):
|
||||
super(InstallApp, self).__init__()
|
||||
self.app = app
|
||||
self.manager = manager
|
||||
self.icon_cache = icon_cache
|
||||
self.disk_cache = disk_cache
|
||||
self.locale_keys = locale_keys
|
||||
self.root_password = None
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.app:
|
||||
subproc = self.manager.install_and_stream(self.app.model)
|
||||
self.signal_output.emit(' '.join(subproc.args) + '\n')
|
||||
success = False
|
||||
|
||||
for output in subproc.stdout:
|
||||
line = output.decode().strip()
|
||||
if line:
|
||||
self.signal_output.emit(line)
|
||||
try:
|
||||
process = self.manager.install_and_stream(self.app.model, self.root_password)
|
||||
success = self.notify_subproc_outputs(process, self.signal_output)
|
||||
|
||||
error = False
|
||||
if success and self.disk_cache:
|
||||
self.app.model.installed = True
|
||||
|
||||
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)
|
||||
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)
|
||||
except (requests.exceptions.ConnectionError, NoInternetException):
|
||||
success = False
|
||||
self.signal_output.emit(self.locale_keys['internet.required'])
|
||||
finally:
|
||||
self.app = None
|
||||
self.signal_finished.emit(success)
|
||||
|
||||
|
||||
class AnimateProgress(QThread):
|
||||
@@ -292,3 +303,30 @@ class VerifyModels(QThread):
|
||||
break
|
||||
|
||||
self.apps = None
|
||||
|
||||
|
||||
class RefreshApp(AsyncAction):
|
||||
|
||||
signal_finished = pyqtSignal(bool)
|
||||
signal_output = pyqtSignal(str)
|
||||
|
||||
def __init__(self, manager: ApplicationManager, app: ApplicationView = None):
|
||||
super(RefreshApp, self).__init__()
|
||||
self.app = app
|
||||
self.manager = manager
|
||||
self.root_password = None
|
||||
|
||||
def run(self):
|
||||
|
||||
if self.app:
|
||||
success = False
|
||||
|
||||
try:
|
||||
process = self.manager.refresh(self.app.model, self.root_password)
|
||||
success = self.notify_subproc_outputs(process, self.signal_output)
|
||||
except (requests.exceptions.ConnectionError, NoInternetException):
|
||||
success = False
|
||||
self.signal_output.emit(self.locale_keys['internet.required'])
|
||||
finally:
|
||||
self.app = None
|
||||
self.signal_finished.emit(success)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
from fpakman.core.model import Application, ApplicationStatus
|
||||
from fpakman.util import util
|
||||
from fpakman.core.model import Application, ApplicationStatus, FlatpakApplication
|
||||
|
||||
|
||||
class ApplicationViewStatus(Enum):
|
||||
@@ -19,7 +19,7 @@ class ApplicationView:
|
||||
|
||||
def get_async_attr(self, attr: str, strip_html: bool = False, default: str = '...'):
|
||||
|
||||
if isinstance(self.model, FlatpakApplication) and self.model.runtime:
|
||||
if self.model.is_library():
|
||||
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
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import operator
|
||||
from functools import reduce
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
from typing import List, Set
|
||||
|
||||
from PyQt5.QtCore import QEvent
|
||||
from PyQt5.QtCore import QEvent, Qt
|
||||
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
|
||||
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar
|
||||
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QHBoxLayout
|
||||
|
||||
from fpakman.core import resource
|
||||
from fpakman.core import resource, system
|
||||
from fpakman.core.controller import ApplicationManager
|
||||
from fpakman.core.model import Application
|
||||
from fpakman.util.cache import Cache
|
||||
@@ -17,7 +17,7 @@ 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, VerifyModels
|
||||
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp
|
||||
from fpakman.view.qt.view_model import ApplicationView
|
||||
|
||||
DARK_ORANGE = '#FF4500'
|
||||
@@ -26,7 +26,7 @@ DARK_ORANGE = '#FF4500'
|
||||
class ManageWindow(QWidget):
|
||||
__BASE_HEIGHT__ = 400
|
||||
|
||||
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, tray_icon=None):
|
||||
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, tray_icon=None):
|
||||
super(ManageWindow, self).__init__()
|
||||
self.locale_keys = locale_keys
|
||||
self.manager = manager
|
||||
@@ -37,6 +37,8 @@ class ManageWindow(QWidget):
|
||||
self.label_flatpak = None
|
||||
self.icon_cache = icon_cache
|
||||
self.disk_cache = disk_cache
|
||||
self.download_icons = download_icons
|
||||
self.screen_size = screen_size
|
||||
|
||||
self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
|
||||
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
|
||||
@@ -46,14 +48,20 @@ class ManageWindow(QWidget):
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.toolbar_top = QToolBar()
|
||||
self.toolbar_top.addWidget(self._new_spacer())
|
||||
|
||||
self.label_status = QLabel()
|
||||
self.label_status.setText('')
|
||||
self.label_status.setStyleSheet("font-weight: bold")
|
||||
self.toolbar_top.addWidget(self.label_status)
|
||||
|
||||
self.toolbar_search = QToolBar()
|
||||
self.toolbar_search.setStyleSheet("spacing: 0px;")
|
||||
self.toolbar_search.setContentsMargins(0, 0, 0, 0)
|
||||
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()
|
||||
@@ -69,30 +77,33 @@ 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)
|
||||
|
||||
self.toolbar_search.addWidget(self._new_spacer())
|
||||
self.layout.addWidget(self.toolbar_search)
|
||||
self.ref_toolbar_search = self.toolbar_top.addWidget(self.toolbar_search)
|
||||
self.toolbar_top.addWidget(self._new_spacer())
|
||||
self.layout.addWidget(self.toolbar_top)
|
||||
|
||||
toolbar = QToolBar()
|
||||
|
||||
self.checkbox_only_apps = QCheckBox()
|
||||
self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps'])
|
||||
self.checkbox_only_apps.setChecked(True)
|
||||
self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps)
|
||||
toolbar.addWidget(self.checkbox_only_apps)
|
||||
self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps)
|
||||
self.ref_checkbox_only_apps = toolbar.addWidget(self.checkbox_only_apps)
|
||||
|
||||
toolbar.addWidget(self._new_spacer())
|
||||
self.checkbox_updates = QCheckBox()
|
||||
self.checkbox_updates.setText(self.locale_keys['updates'].capitalize())
|
||||
self.checkbox_updates.stateChanged.connect(self._handle_updates_filter)
|
||||
self.ref_checkbox_updates = toolbar.addWidget(self.checkbox_updates)
|
||||
|
||||
self.label_status = QLabel()
|
||||
self.label_status.setText('')
|
||||
self.label_status.setStyleSheet("font-weight: bold")
|
||||
toolbar.addWidget(self.label_status)
|
||||
self.extra_filters = QWidget()
|
||||
self.extra_filters.setLayout(QHBoxLayout())
|
||||
toolbar.addWidget(self.extra_filters)
|
||||
|
||||
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(keep_console=False))
|
||||
self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
|
||||
toolbar.addWidget(self.bt_refresh)
|
||||
|
||||
self.bt_upgrade = QToolButton()
|
||||
@@ -104,7 +115,7 @@ class ManageWindow(QWidget):
|
||||
|
||||
self.layout.addWidget(toolbar)
|
||||
|
||||
self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache)
|
||||
self.table_apps = AppsTable(self, self.icon_cache, disk_cache=self.disk_cache, download_icons=self.download_icons)
|
||||
self.table_apps.change_headers_policy()
|
||||
|
||||
self.layout.addWidget(self.table_apps)
|
||||
@@ -132,7 +143,7 @@ class ManageWindow(QWidget):
|
||||
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_refresh.signal.connect(self._finish_refresh_apps)
|
||||
|
||||
self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
|
||||
self.thread_uninstall.signal_output.connect(self._update_action_output)
|
||||
@@ -151,7 +162,7 @@ class ManageWindow(QWidget):
|
||||
self.thread_search = SearchApps(self.manager)
|
||||
self.thread_search.signal_finished.connect(self._finish_search)
|
||||
|
||||
self.thread_install = InstallApp(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache)
|
||||
self.thread_install = InstallApp(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, locale_keys=self.locale_keys)
|
||||
self.thread_install.signal_output.connect(self._update_action_output)
|
||||
self.thread_install.signal_finished.connect(self._finish_install)
|
||||
|
||||
@@ -161,10 +172,11 @@ class ManageWindow(QWidget):
|
||||
self.thread_verify_models = VerifyModels()
|
||||
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)
|
||||
|
||||
self.thread_refresh_app = RefreshApp(manager=self.manager)
|
||||
self.thread_refresh_app.signal_finished.connect(self._finish_refresh)
|
||||
self.thread_refresh_app.signal_output.connect(self._update_action_output)
|
||||
|
||||
self.toolbar_bottom = QToolBar()
|
||||
self.label_updates = QLabel('')
|
||||
self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE))
|
||||
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
|
||||
|
||||
self.toolbar_bottom.addWidget(self._new_spacer())
|
||||
|
||||
@@ -174,10 +186,34 @@ class ManageWindow(QWidget):
|
||||
|
||||
self.toolbar_bottom.addWidget(self._new_spacer())
|
||||
|
||||
self.label_updates = QLabel()
|
||||
self.ref_label_updates = self.toolbar_bottom.addWidget(self.label_updates)
|
||||
|
||||
self.layout.addWidget(self.toolbar_bottom)
|
||||
|
||||
self.centralize()
|
||||
|
||||
self.filter_only_apps = True
|
||||
self.filter_types = set()
|
||||
self.filter_updates = False
|
||||
|
||||
def _handle_updates_filter(self, status: int):
|
||||
self.filter_updates = status == 2
|
||||
self.apply_filters()
|
||||
|
||||
def _handle_filter_only_apps(self, status: int):
|
||||
self.filter_only_apps = status == 2
|
||||
self.apply_filters()
|
||||
|
||||
def _handle_type_filter(self, status: int, app_type: str):
|
||||
|
||||
if status == 2:
|
||||
self.filter_types.add(app_type)
|
||||
elif app_type in self.filter_types:
|
||||
self.filter_types.remove(app_type)
|
||||
|
||||
self.apply_filters()
|
||||
|
||||
def _notify_model_data_change(self):
|
||||
self.table_apps.fill_async_data()
|
||||
|
||||
@@ -234,61 +270,136 @@ class ManageWindow(QWidget):
|
||||
self.checkbox_console.setChecked(False)
|
||||
self.textarea_output.hide()
|
||||
|
||||
def refresh(self, keep_console: bool = True):
|
||||
def refresh_apps(self, keep_console: bool = True):
|
||||
|
||||
if self._acquire_lock():
|
||||
self.filter_types.clear()
|
||||
self.input_search.clear()
|
||||
|
||||
if not keep_console:
|
||||
self._handle_console_option(False)
|
||||
|
||||
self._begin_action(self.locale_keys['manage_window.status.refreshing'])
|
||||
self.ref_checkbox_updates.setVisible(False)
|
||||
self.ref_checkbox_only_apps.setVisible(False)
|
||||
self._begin_action(self.locale_keys['manage_window.status.refreshing'], clear_filters=True)
|
||||
|
||||
self.thread_refresh.start()
|
||||
|
||||
def _finish_refresh(self, apps: List[Application]):
|
||||
def _finish_refresh_apps(self, apps: List[Application]):
|
||||
self.update_apps(apps)
|
||||
self.finish_action()
|
||||
self.ref_checkbox_only_apps.setVisible(True)
|
||||
self._release_lock()
|
||||
|
||||
def uninstall_app(self, app: ApplicationView):
|
||||
if self._acquire_lock():
|
||||
|
||||
pwd = None
|
||||
requires_root = self.manager.requires_root('uninstall', self.table_apps.get_selected_app().model)
|
||||
|
||||
if not is_root() and requires_root:
|
||||
pwd, ok = ask_root_password(self.locale_keys)
|
||||
|
||||
if not ok:
|
||||
self._release_lock()
|
||||
return
|
||||
|
||||
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 = app
|
||||
self.thread_uninstall.root_password = pwd
|
||||
self.thread_uninstall.start()
|
||||
|
||||
def _finish_uninstall(self):
|
||||
def refresh(self, app: ApplicationView):
|
||||
if self._acquire_lock():
|
||||
|
||||
pwd = None
|
||||
requires_root = self.manager.requires_root('refresh', self.table_apps.get_selected_app().model)
|
||||
|
||||
if not is_root() and requires_root:
|
||||
pwd, ok = ask_root_password(self.locale_keys)
|
||||
|
||||
if not ok:
|
||||
self._release_lock()
|
||||
return
|
||||
|
||||
self._handle_console_option(True)
|
||||
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.refreshing'], app.model.base_data.name))
|
||||
|
||||
self.thread_refresh_app.app = app
|
||||
self.thread_refresh_app.root_password = pwd
|
||||
self.thread_refresh_app.start()
|
||||
|
||||
def _finish_uninstall(self, success: bool):
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
self.refresh()
|
||||
|
||||
if success:
|
||||
if self._can_notify_user():
|
||||
app = self.table_apps.get_selected_app()
|
||||
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['uninstalled']))
|
||||
|
||||
self.refresh_apps()
|
||||
else:
|
||||
if self._can_notify_user():
|
||||
app = self.table_apps.get_selected_app()
|
||||
system.notify_user('{}: {}'.format(app.model.base_data.name, self.locale_keys['notification.uninstall.failed']))
|
||||
|
||||
self.checkbox_console.setChecked(True)
|
||||
|
||||
def _can_notify_user(self):
|
||||
return self.isHidden() or self.isMinimized()
|
||||
|
||||
def _finish_downgrade(self, success: bool):
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
|
||||
if success:
|
||||
self.refresh()
|
||||
if self._can_notify_user():
|
||||
app = self.table_apps.get_selected_app()
|
||||
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['downgraded']))
|
||||
|
||||
self.refresh_apps()
|
||||
else:
|
||||
if self._can_notify_user():
|
||||
system.notify_user(self.locale_keys['notification.downgrade.failed'])
|
||||
|
||||
self.change_update_state(notify_tray=False)
|
||||
self.checkbox_console.setChecked(True)
|
||||
|
||||
def filter_only_apps(self, only_apps: int):
|
||||
def _finish_refresh(self, success: bool):
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
|
||||
if success:
|
||||
self.refresh_apps()
|
||||
else:
|
||||
self.change_update_state(notify_tray=False)
|
||||
self.checkbox_console.setChecked(True)
|
||||
|
||||
def apply_filters(self):
|
||||
if self.apps:
|
||||
show_only_apps = True if only_apps == 2 else False
|
||||
|
||||
visible_apps = len(self.apps)
|
||||
for idx, app_v in enumerate(self.apps):
|
||||
hidden = show_only_apps and app_v.model.is_library()
|
||||
hidden = self.filter_only_apps and app_v.model.is_library()
|
||||
|
||||
if not hidden and self.filter_types is not None:
|
||||
hidden = app_v.model.get_type() not in self.filter_types
|
||||
|
||||
if not hidden and self.filter_updates:
|
||||
hidden = not app_v.model.update
|
||||
|
||||
self.table_apps.setRowHidden(idx, hidden)
|
||||
app_v.visible = not hidden
|
||||
visible_apps -= 1 if hidden else 0
|
||||
|
||||
self.change_update_state()
|
||||
self.change_update_state(change_filters=False)
|
||||
self.table_apps.change_headers_policy(QHeaderView.Stretch)
|
||||
self.table_apps.change_headers_policy()
|
||||
self.resize_and_center()
|
||||
self.resize_and_center(accept_lower_width=visible_apps > 0)
|
||||
|
||||
def change_update_state(self):
|
||||
def change_update_state(self, notify_tray: bool = True, change_filters: bool = True):
|
||||
|
||||
enable_bt_update = False
|
||||
|
||||
@@ -301,23 +412,38 @@ class ManageWindow(QWidget):
|
||||
else:
|
||||
app_updates += 1
|
||||
|
||||
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('{} {} | {} {}'.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_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])
|
||||
|
||||
total_updates = app_updates + library_updates
|
||||
if total_updates > 0:
|
||||
self.label_updates.setPixmap(QPixmap(resource.get_path('img/exclamation.svg')).scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||
self.label_updates.setToolTip('{}: {} ( {} {} | {} {} )'.format(self.locale_keys['manage_window.label.updates'],
|
||||
total_updates,
|
||||
app_updates,
|
||||
self.locale_keys['manage_window.checkbox.only_apps'].lower(),
|
||||
library_updates,
|
||||
self.locale_keys['others'].lower()))
|
||||
|
||||
if not self.ref_checkbox_updates.isVisible():
|
||||
self.ref_checkbox_updates.setVisible(True)
|
||||
|
||||
if change_filters and not self.checkbox_updates.isChecked():
|
||||
self.checkbox_updates.setChecked(True)
|
||||
|
||||
if change_filters and library_updates > 0 and self.checkbox_only_apps.isChecked():
|
||||
self.checkbox_only_apps.setChecked(False)
|
||||
else:
|
||||
self.checkbox_updates.setChecked(False)
|
||||
self.ref_checkbox_updates.setVisible(False)
|
||||
self.label_updates.setPixmap(QPixmap())
|
||||
|
||||
if notify_tray:
|
||||
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
|
||||
|
||||
def centralize(self):
|
||||
geo = self.frameGeometry()
|
||||
@@ -330,12 +456,12 @@ class ManageWindow(QWidget):
|
||||
self.apps = []
|
||||
|
||||
napps = 0 # number of apps (not libraries)
|
||||
available_types = set()
|
||||
|
||||
if apps:
|
||||
for app in apps:
|
||||
app_model = ApplicationView(model=app,
|
||||
visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked())
|
||||
|
||||
app_model = ApplicationView(model=app, visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked())
|
||||
available_types.add(app.get_type())
|
||||
napps += 1 if not app.is_library() else 0
|
||||
self.apps.append(app_model)
|
||||
|
||||
@@ -346,17 +472,41 @@ class ManageWindow(QWidget):
|
||||
self.checkbox_only_apps.setCheckable(True)
|
||||
self.checkbox_only_apps.setChecked(True)
|
||||
|
||||
self._update_type_filters(available_types)
|
||||
self.table_apps.update_apps(self.apps)
|
||||
self.apply_filters()
|
||||
self.change_update_state()
|
||||
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):
|
||||
def _update_type_filters(self, available_types: Set[str]):
|
||||
|
||||
self.filter_types = available_types
|
||||
|
||||
filters_layout = self.extra_filters.layout()
|
||||
for i in reversed(range(filters_layout.count())):
|
||||
filters_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
if available_types:
|
||||
for app_type in sorted(list(available_types)):
|
||||
checkbox_app_type = QCheckBox()
|
||||
checkbox_app_type.setChecked(True)
|
||||
checkbox_app_type.setText(app_type)
|
||||
|
||||
def handle_click(status: int, filter_type: str = app_type):
|
||||
self._handle_type_filter(status, filter_type)
|
||||
|
||||
checkbox_app_type.stateChanged.connect(handle_click)
|
||||
filters_layout.addWidget(checkbox_app_type)
|
||||
|
||||
def resize_and_center(self, accept_lower_width: bool = True):
|
||||
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())
|
||||
|
||||
if accept_lower_width or new_width > self.width():
|
||||
self.resize(new_width, self.height())
|
||||
|
||||
self.centralize()
|
||||
|
||||
def update_selected(self):
|
||||
@@ -373,20 +523,26 @@ class ManageWindow(QWidget):
|
||||
self.thread_update.apps_to_update = to_update
|
||||
self.thread_update.start()
|
||||
|
||||
def _finish_update_selected(self, success: bool):
|
||||
def _finish_update_selected(self, success: bool, updated: int):
|
||||
self.finish_action()
|
||||
self._release_lock()
|
||||
|
||||
if success:
|
||||
self.refresh()
|
||||
if self._can_notify_user():
|
||||
system.notify_user('{} {}'.format(updated, self.locale_keys['notification.update_selected.success']))
|
||||
|
||||
self.refresh_apps()
|
||||
else:
|
||||
if self._can_notify_user():
|
||||
system.notify_user(self.locale_keys['notification.update_selected.failed'])
|
||||
|
||||
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, keep_search: bool = False):
|
||||
def _begin_action(self, action_label: str, keep_search: bool = False, clear_filters: bool = False):
|
||||
self.ref_label_updates.setVisible(False)
|
||||
self.thread_animate_progress.stop = False
|
||||
self.thread_animate_progress.start()
|
||||
@@ -397,11 +553,17 @@ class ManageWindow(QWidget):
|
||||
self.bt_refresh.setEnabled(False)
|
||||
self.checkbox_only_apps.setEnabled(False)
|
||||
self.table_apps.setEnabled(False)
|
||||
self.checkbox_updates.setEnabled(False)
|
||||
|
||||
if keep_search:
|
||||
self.toolbar_search.setEnabled(False)
|
||||
self.ref_toolbar_search.setVisible(True)
|
||||
else:
|
||||
self.toolbar_search.setVisible(False)
|
||||
self.ref_toolbar_search.setVisible(False)
|
||||
|
||||
if clear_filters:
|
||||
self._update_type_filters(set())
|
||||
else:
|
||||
self.extra_filters.setEnabled(False)
|
||||
|
||||
def finish_action(self):
|
||||
self.ref_progress_bar.setVisible(False)
|
||||
@@ -413,15 +575,18 @@ class ManageWindow(QWidget):
|
||||
self.table_apps.setEnabled(True)
|
||||
self.input_search.setEnabled(True)
|
||||
self.label_status.setText('')
|
||||
self.toolbar_search.setVisible(True)
|
||||
self.toolbar_search.setEnabled(True)
|
||||
self.ref_toolbar_search.setVisible(True)
|
||||
self.ref_toolbar_search.setEnabled(True)
|
||||
self.extra_filters.setEnabled(True)
|
||||
self.checkbox_updates.setEnabled(True)
|
||||
|
||||
def downgrade_app(self, app: ApplicationView):
|
||||
if self._acquire_lock():
|
||||
|
||||
pwd = None
|
||||
requires_root = self.manager.requires_root('downgrade', self.table_apps.get_selected_app().model)
|
||||
|
||||
if not is_root():
|
||||
if not is_root() and requires_root:
|
||||
pwd, ok = ask_root_password(self.locale_keys)
|
||||
|
||||
if not ok:
|
||||
@@ -456,7 +621,7 @@ class ManageWindow(QWidget):
|
||||
self._release_lock()
|
||||
self.finish_action()
|
||||
self.change_update_state()
|
||||
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys)
|
||||
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys, self.table_apps.get_selected_app().model.get_type(), self.screen_size)
|
||||
dialog_info.exec_()
|
||||
|
||||
def _finish_get_history(self, app: dict):
|
||||
@@ -478,7 +643,10 @@ class ManageWindow(QWidget):
|
||||
|
||||
if word and self._acquire_lock():
|
||||
self._handle_console_option(False)
|
||||
self._begin_action(self.locale_keys['manage_window.status.searching'], keep_search=True)
|
||||
self.ref_checkbox_only_apps.setVisible(False)
|
||||
self.ref_checkbox_updates.setVisible(False)
|
||||
self.filter_updates = False
|
||||
self._begin_action('{} "{}"'.format(self.locale_keys['manage_window.status.searching'], word), clear_filters=True)
|
||||
self.thread_search.word = word
|
||||
self.thread_search.start()
|
||||
|
||||
@@ -489,10 +657,22 @@ class ManageWindow(QWidget):
|
||||
|
||||
def install_app(self, app: ApplicationView):
|
||||
if self._acquire_lock():
|
||||
|
||||
pwd = None
|
||||
requires_root = self.manager.requires_root('install', self.table_apps.get_selected_app().model)
|
||||
|
||||
if not is_root() and requires_root:
|
||||
pwd, ok = ask_root_password(self.locale_keys)
|
||||
|
||||
if not ok:
|
||||
self._release_lock()
|
||||
return
|
||||
|
||||
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.root_password = pwd
|
||||
self.thread_install.start()
|
||||
|
||||
def _finish_install(self, success: bool):
|
||||
@@ -501,8 +681,16 @@ class ManageWindow(QWidget):
|
||||
self._release_lock()
|
||||
|
||||
if success:
|
||||
self.refresh()
|
||||
if self._can_notify_user():
|
||||
app = self.table_apps.get_selected_app()
|
||||
system.notify_user('{} ({}) {}'.format(app.model.base_data.name, app.model.get_type(), self.locale_keys['installed']))
|
||||
|
||||
self.refresh_apps()
|
||||
else:
|
||||
if self._can_notify_user():
|
||||
app = self.table_apps.get_selected_app()
|
||||
system.notify_user('{}: {}'.format(app.model.base_data.name, self.locale_keys['notification.install.failed']))
|
||||
|
||||
self.checkbox_console.setChecked(True)
|
||||
|
||||
def _update_progress(self, value: int):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pyqt5>=5.12
|
||||
requests>=2.22
|
||||
colorama>=0.4.1
|
||||
colorama>=0.4.1
|
||||
beautifulsoup4>=4.7
|
||||
Reference in New Issue
Block a user