This commit is contained in:
Vinícius Moreira
2019-07-25 12:31:39 -03:00
committed by GitHub
41 changed files with 1941 additions and 793 deletions

View File

@@ -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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.4.0] - 2019-07-25
### 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 ## [0.3.1] - 2019-07-13
### Improvements ### Improvements
- Console output now is optional and not shown by default - Console output now is optional and not shown by default

View File

@@ -1,6 +1,6 @@
## fpakman ## 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. an application management panel where you can search, update, install and uninstall applications.
### Developed with: ### Developed with:
@@ -48,8 +48,20 @@ 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_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_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_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 ### Roadmap
- Support for other packaging technologies - Support for other packaging technologies
- Separate modules for each packaging technology
- Memory and performance improvements - Memory and performance improvements
- Improve user experience

View File

@@ -8,7 +8,7 @@ desktop_file = """
Type = Application Type = Application
Name = fpakman Name = fpakman
Categories = System; Categories = System;
Comment = Manage your Flatpak applications Comment = Manage your Flatpak / Snap applications
Exec = /usr/bin/fpakman Exec = /usr/bin/fpakman
Icon = /usr/lib/python{version}/site-packages/fpakman/resources/img/logo.svg Icon = /usr/lib/python{version}/site-packages/fpakman/resources/img/logo.svg
""".format(version="{}.{}".format(sys.version_info.major, sys.version_info.minor)) """.format(version="{}.{}".format(sys.version_info.major, sys.version_info.minor))

View File

@@ -1,2 +1,5 @@
__version__ = '0.3.1' __version__ = '0.4.0'
__app_name__ = 'fpakman' __app_name__ = 'fpakman'
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

View File

@@ -1,17 +1,24 @@
import argparse import argparse
import os import os
import sys import sys
from pathlib import Path
import requests
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from colorama import Fore from colorama import Fore
from fpakman import __version__, __app_name__ from fpakman import __version__, __app_name__
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.controller import GenericApplicationManager
from fpakman.core.disk import DiskCacheLoaderFactory 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.util import util
from fpakman.core.controller import FlatpakManager, GenericApplicationManager
from fpakman.util.cache import Cache from fpakman.util.cache import Cache
from fpakman.util.memory import CacheCleaner from fpakman.util.memory import CacheCleaner
from fpakman.view.qt.systray import TrayIcon 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('-ie', '--icon-exp', action="store", default=int(os.getenv('FPAKMAN_ICON_EXPIRATION', 60 * 5)), type=int, help='cached icons expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s') parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s')
parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s') parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s')
parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enables / disables system notifications for new updates. 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", 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('-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() args = parser.parse_args()
if args.cache_exp < 0: 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) log_msg("'check-interval' set as '{}'. It must be >= 0. Aborting...".format(args.check_interval), Fore.RED)
exit(1) 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: if args.update_notification == 0:
log_msg('updates notifications are disabled', Fore.YELLOW) 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) locale_keys = util.get_locale_keys(args.locale)
prepare_folder_structure(disk_cache=args.disk_cache) http_session = requests.Session()
caches = [] caches = []
flatpak_api_cache = Cache(expiration_time=args.cache_exp) cache_map = {}
caches.append(flatpak_api_cache) 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) icon_cache = Cache(expiration_time=args.icon_exp)
caches.append(icon_cache) 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 = QApplication(sys.argv)
app.setApplicationName(__app_name__)
app.setApplicationVersion(__version__)
app.setWindowIcon(QIcon(resource.get_path('img/logo.svg'))) 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, trayIcon = TrayIcon(locale_keys=locale_keys,
manager=manager, manager=manager,
check_interval=args.check_interval, check_interval=args.check_interval,
icon_cache=icon_cache, icon_cache=icon_cache,
disk_cache=args.disk_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() trayIcon.show()
CacheCleaner(caches).start() CacheCleaner(caches).start()

View File

@@ -1,2 +1,6 @@
FLATHUB_URL = 'https://flathub.org' from pathlib import Path
FLATHUB_API_URL = FLATHUB_URL + '/api/v1'
from fpakman import __app_name__
HOME_PATH = Path.home()
CACHE_PATH = '{}/.cache/{}'.format(HOME_PATH, __app_name__)

View File

@@ -1,30 +1,27 @@
import os
import shutil
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from argparse import Namespace
from threading import Lock from typing import List, Dict
from typing import List
import requests
from fpakman.core import flatpak, disk
from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory
from fpakman.core.model import FlatpakApplication, ApplicationData, ApplicationStatus, Application from fpakman.core.model import Application, ApplicationUpdate
from fpakman.util.cache import Cache from fpakman.core.system import FpakmanProcess
class ApplicationManager(ABC): class ApplicationManager(ABC):
def __init__(self, app_args):
self.app_args = app_args
@abstractmethod @abstractmethod
def search(self, word: str, disk_loader: DiskCacheLoader) -> List[Application]: def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[Application]]:
pass pass
@abstractmethod @abstractmethod
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool) -> List[Application]: def read_installed(self, disk_loader: DiskCacheLoader) -> List[Application]:
pass pass
@abstractmethod @abstractmethod
def downgrade_app(self, app: Application, root_password: str): def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess:
pass pass
@abstractmethod @abstractmethod
@@ -36,15 +33,15 @@ class ApplicationManager(ABC):
pass pass
@abstractmethod @abstractmethod
def update_and_stream(self, app: Application): def update_and_stream(self, app: Application) -> FpakmanProcess:
pass pass
@abstractmethod @abstractmethod
def uninstall_and_stream(self, app: Application): def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
pass pass
@abstractmethod @abstractmethod
def get_app_type(self) -> str: def get_app_type(self):
pass pass
@abstractmethod @abstractmethod
@@ -56,7 +53,7 @@ class ApplicationManager(ABC):
pass pass
@abstractmethod @abstractmethod
def install_and_stream(self, app: Application): def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess:
pass pass
@abstractmethod @abstractmethod
@@ -67,194 +64,121 @@ class ApplicationManager(ABC):
def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool): def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool):
pass 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): @abstractmethod
def list_updates(self) -> List[ApplicationUpdate]:
def __init__(self, api_cache: Cache, disk_cache: bool): pass
self.api_cache = api_cache
self.http_session = requests.Session()
self.lock_read = Lock()
self.disk_cache = disk_cache
self.async_data_loader = FlatpakAsyncDataLoaderManager(api_cache=self.api_cache, manager=self)
flatpak.set_default_remotes()
def get_app_type(self):
return FlatpakApplication
def _map_to_model(self, app: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication:
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
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]:
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()
return res
def read_installed(self, disk_loader: DiskCacheLoader, keep_workers: bool = False) -> List[FlatpakApplication]:
self.lock_read.acquire()
try:
installed = flatpak.list_installed()
if installed:
installed.sort(key=lambda p: p['name'].lower())
available_updates = flatpak.list_updates_as_str()
models = []
for app in installed:
model = self._map_to_model(app, True, disk_loader)
model.update = app['id'] in available_updates
models.append(model)
if not keep_workers:
self.async_data_loader.stop_current_workers()
return models
return []
finally:
self.lock_read.release()
def 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): class GenericApplicationManager(ApplicationManager):
def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory): def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory, app_args: Namespace):
super(ApplicationManager, self).__init__()
self.managers = managers self.managers = managers
self.map = {m.get_app_type(): m for m in self.managers} self.map = {m.get_app_type(): m for m in self.managers}
self.disk_loader_factory = disk_loader_factory self.disk_loader_factory = disk_loader_factory
self._enabled_map = {} if app_args.check_packaging_once else None
self.prepare()
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> List[Application]: def _sort(self, apps: List[Application], word: str) -> List[Application]:
apps = []
disk_loader = self.disk_loader_factory.new() exact_name_matches, contains_name_matches, desc_name_matches, others = [], [], [], []
disk_loader.start()
for app in apps:
lower_name = app.base_data.name.lower()
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 = []
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 _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]]:
res = {'installed': [], 'new': []}
norm_word = word.strip().lower()
disk_loader = None
for man in self.managers: for man in self.managers:
if man.is_enabled(): if self._is_enabled(man):
apps.extend(man.search(word, disk_loader)) 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.stop = True
disk_loader.join() disk_loader.join()
return apps
def read_installed(self, disk_loader: DiskCacheLoader = None, keep_workers: bool = False) -> List[Application]: for key in res:
res[key] = self._sort(res[key], norm_word)
return res
def read_installed(self, disk_loader: DiskCacheLoader = None) -> List[Application]:
installed = [] installed = []
disk_loader = self.disk_loader_factory.new() disk_loader = None
disk_loader.start()
for man in self.managers: for man in self.managers:
if man.is_enabled(): if self._is_enabled(man):
installed.extend(man.read_installed(disk_loader=disk_loader, keep_workers=keep_workers)) if not disk_loader:
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
installed.extend(man.read_installed(disk_loader=disk_loader))
disk_loader.stop = True disk_loader.stop = True
disk_loader.join() disk_loader.join()
installed.sort(key=lambda a: a.base_data.name.lower())
return installed return installed
def can_downgrade(self): def can_downgrade(self):
return True 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) man = self._get_manager_for(app)
if man and man.can_downgrade(): if man and man.can_downgrade():
@@ -268,23 +192,23 @@ class GenericApplicationManager(ApplicationManager):
if man: if man:
return man.clean_cache_for(app) 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) man = self._get_manager_for(app)
if man: if man:
return man.update_and_stream(app) 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) man = self._get_manager_for(app)
if man: 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) man = self._get_manager_for(app)
if man: if man:
return man.install_and_stream(app) return man.install_and_stream(app, root_password)
def get_info(self, app: Application): def get_info(self, app: Application):
man = self._get_manager_for(app) man = self._get_manager_for(app)
@@ -306,7 +230,7 @@ class GenericApplicationManager(ApplicationManager):
def _get_manager_for(self, app: Application) -> ApplicationManager: def _get_manager_for(self, app: Application) -> ApplicationManager:
man = self.map[app.__class__] 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): 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(): if self.disk_loader_factory.disk_cache and app.supports_disk_cache():
@@ -314,3 +238,31 @@ class GenericApplicationManager(ApplicationManager):
if man: if man:
return man.cache_to_disk(app, icon_bytes=icon_bytes, only_icon=only_icon) 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()
def list_updates(self) -> List[ApplicationUpdate]:
updates = []
if self.managers:
for man in self.managers:
if self._is_enabled(man):
updates.extend(man.list_updates())
return updates

View File

@@ -2,20 +2,20 @@ import json
import os import os
from pathlib import Path from pathlib import Path
from threading import Thread, Lock 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 from fpakman.util.cache import Cache
class DiskCacheLoader(Thread): 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) super(DiskCacheLoader, self).__init__(daemon=True)
self.apps = apps self.apps = apps
self.stop = False self.stop = False
self.lock = Lock() self.lock = Lock()
self.flatpak_api_cache = flatpak_api_cache self.cache_map = cache_map
self.enabled = enabled self.enabled = enabled
def run(self): def run(self):
@@ -44,19 +44,17 @@ class DiskCacheLoader(Thread):
with open(app.get_disk_data_path()) as f: with open(app.get_disk_data_path()) as f:
cached_data = json.loads(f.read()) cached_data = json.loads(f.read())
app.fill_cached_data(cached_data) app.fill_cached_data(cached_data)
self.cache_map.get(app.__class__).add_non_existing(app.base_data.id, cached_data)
if isinstance(app, FlatpakApplication):
self.flatpak_api_cache.add_non_existing(app.base_data.id, cached_data)
class DiskCacheLoaderFactory: 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.disk_cache = disk_cache
self.flatpak_api_cache = flatpak_api_cache self.cache_map = cache_map
def new(self): 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): 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: if not only_icon:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)
data = app.get_data_to_cache()
if isinstance(app, FlatpakApplication): with open(app.get_disk_data_path(), 'w+') as f:
data = app.get_data_to_cache() f.write(json.dumps(data))
with open(app.get_disk_data_path(), 'w+') as f:
f.write(json.dumps(data))
if icon_bytes: if icon_bytes:
Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True)

View File

View 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)

View File

@@ -0,0 +1,180 @@
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, FlatpakUpdateLoader
from fpakman.core.model import ApplicationData, ApplicationUpdate
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()
def list_updates(self) -> List[ApplicationUpdate]:
updates = []
installed = flatpak.list_installed(extra_fields=False)
if installed:
available_updates = flatpak.list_updates_as_str()
if available_updates:
loaders = None
for app_json in installed:
if app_json['id'] in available_updates:
loader = FlatpakUpdateLoader(app=app_json, http_session=self.http_session)
loader.start()
if loaders is None:
loaders = []
loaders.append(loader)
if loaders:
for loader in loaders:
loader.join()
app = loader.app
updates.append(ApplicationUpdate(app_id='{}:{}'.format(app['id'], app['branch']),
app_type='flatpak',
version=app.get('version')))
return updates

View File

@@ -1,15 +1,14 @@
import re import re
import subprocess import subprocess
from typing import List from typing import List, Set
from fpakman.core import system from fpakman.core import system
from fpakman.core.exception import NoInternetException from fpakman.core.exception import NoInternetException
from fpakman.core.model import Application
BASE_CMD = 'flatpak' BASE_CMD = 'flatpak'
def app_str_to_json(line: str, version: str) -> dict: def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict:
app_array = line.split('\t') app_array = line.split('\t')
@@ -18,18 +17,21 @@ def app_str_to_json(line: str, version: str) -> dict:
'id': app_array[1], 'id': app_array[1],
'version': app_array[2], 'version': app_array[2],
'branch': app_array[3]} 'branch': app_array[3]}
elif '1.0' <= version < '1.1':
app = {'ref': app_array[0], 'options': app_array[1]}
ref_data = app['ref'].split('/') elif '1.0' <= version < '1.1':
app['id'] = ref_data[0]
app['arch'] = ref_data[1] ref_data = app_array[0].split('/')
app['branch'] = ref_data[2]
app['name'] = ref_data[0].split('.')[-1] app = {'id': ref_data[0],
app['version'] = None 'arch': ref_data[1],
'name': ref_data[0].split('.')[-1],
'ref': app_array[0],
'options': app_array[1],
'branch': ref_data[2],
'version': None}
elif '1.2' <= version < '1.3': elif '1.2' <= version < '1.3':
app = {'name': app_array[1].strip().split('.')[-1], app = {'id': app_array[1],
'id': app_array[1], 'name': app_array[1].strip().split('.')[-1],
'version': app_array[2], 'version': app_array[2],
'branch': app_array[3], 'branch': app_array[3],
'arch': app_array[4], 'arch': app_array[4],
@@ -37,8 +39,9 @@ def app_str_to_json(line: str, version: str) -> dict:
else: else:
raise Exception('Unsupported version') raise Exception('Unsupported version')
extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True) if extra_fields:
app.update(extra_fields) extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
app.update(extra_fields)
return app return app
@@ -82,13 +85,13 @@ def get_app_info(app_id: str, branch: str):
return system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch)) return system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def list_installed() -> List[dict]: def list_installed(extra_fields: bool = True) -> List[dict]:
apps_str = system.run_cmd('{} list'.format(BASE_CMD)) apps_str = system.run_cmd('{} list'.format(BASE_CMD))
if apps_str: if apps_str:
version = get_version() version = get_version()
app_lines = apps_str.split('\n') app_lines = apps_str.split('\n')
return [app_str_to_json(line, version) for line in app_lines if line] return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line]
return [] return []
@@ -115,16 +118,8 @@ def list_updates_as_str():
return system.run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True) return system.run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True)
def downgrade_and_stream(app_ref: str, commit: str, root_password: str): 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)
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 get_app_commits(app_ref: str, origin: str) -> List[str]: def get_app_commits(app_ref: str, origin: str) -> List[str]:
@@ -224,4 +219,3 @@ def install_and_stream(app_id: str, origin: str):
def set_default_remotes(): def set_default_remotes():
system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo') system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo')

View 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])

View File

@@ -0,0 +1,124 @@
import time
import traceback
from threading import Thread
from colorama import Fore
from fpakman.core.controller import ApplicationManager
from fpakman.core.flatpak import flatpak
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)
class FlatpakUpdateLoader(Thread):
def __init__(self, app: dict, http_session, attempts: int = 2, timeout: int = 20):
super(FlatpakUpdateLoader, self).__init__(daemon=True)
self.app = app
self.http_session = http_session
self.attempts = attempts
self.timeout = timeout
def run(self):
if self.app.get('ref') is None:
self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True))
else:
self.app['runtime'] = self.app['ref'].startswith('runtime/')
if not self.app['runtime']:
current_attempts = 0
while current_attempts < self.attempts:
current_attempts += 1
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']), timeout=self.timeout)
if res.status_code == 200 and res.text:
data = res.json()
if data.get('currentReleaseVersion'):
self.app['version'] = data['currentReleaseVersion']
break
except:
traceback.print_exc()
time.sleep(0.5)

View File

@@ -2,7 +2,6 @@ from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.structure import flatpak_cache_path
class ApplicationStatus(Enum): class ApplicationStatus(Enum):
@@ -49,6 +48,10 @@ class Application(ABC):
def can_be_installed(self): def can_be_installed(self):
pass pass
@abstractmethod
def can_be_refreshed(self):
return self.installed
@abstractmethod @abstractmethod
def get_type(self): def get_type(self):
pass pass
@@ -60,74 +63,12 @@ class Application(ABC):
def is_library(self): def is_library(self):
pass 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): def supports_disk_cache(self):
return self.installed and not self.is_library() return self.installed and not self.is_library()
@abstractmethod
def get_disk_cache_path(self): def get_disk_cache_path(self):
return '{}/{}'.format(flatpak_cache_path, self.base_data.id) pass
def get_disk_icon_path(self): def get_disk_icon_path(self):
return '{}/icon.png'.format(self.get_disk_cache_path()) return '{}/icon.png'.format(self.get_disk_cache_path())
@@ -135,16 +76,24 @@ class FlatpakApplication(Application):
def get_disk_data_path(self): def get_disk_data_path(self):
return '{}/data.json'.format(self.get_disk_cache_path()) return '{}/data.json'.format(self.get_disk_cache_path())
@abstractmethod
def get_data_to_cache(self): def get_data_to_cache(self):
return { pass
'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
}
@abstractmethod
def fill_cached_data(self, data: dict): def fill_cached_data(self, data: dict):
for attr in self.get_data_to_cache().keys(): pass
if not getattr(self.base_data, attr):
setattr(self.base_data, attr, data[attr]) def __str__(self):
return '{} (id={}, name={})'.format(self.__class__.__name__, self.base_data.id, self.base_data.name)
class ApplicationUpdate:
def __init__(self, app_id: str, version: str, app_type: str):
self.id = app_id
self.version = version
self.type = app_type
def __str__(self):
return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type)

View File

@@ -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): def get_path(resource_path):
return app_dir + '/resources/' + resource_path return ROOT_DIR + '/resources/' + resource_path

View File

View 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)

View File

@@ -0,0 +1,136 @@
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, ApplicationUpdate
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
def list_updates(self) -> List[ApplicationUpdate]:
return []

View 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
View 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)

View 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 = {}

View File

@@ -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)

View File

@@ -2,9 +2,18 @@ import os
import subprocess import subprocess
from typing import List from typing import List
from fpakman import __app_name__
from fpakman.core import resource 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: def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str:
args = { args = {
"shell": True, "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')): 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)

View File

@@ -1,29 +1,17 @@
import traceback
from io import StringIO from io import StringIO
from threading import Thread from threading import Thread
from typing import List
import requests
from colorama import Fore from colorama import Fore
from fpakman.core.constants import FLATHUB_API_URL, FLATHUB_URL from fpakman.core.model import Application
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import FlatpakApplication, ApplicationStatus
from fpakman.util.cache import Cache
class FlatpakAsyncDataLoader(Thread): class AsyncDataLoader(Thread):
def __init__(self, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 3, apps: List[FlatpakApplication] = []): def __init__(self, app: Application):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True) super(AsyncDataLoader, self).__init__(daemon=True)
self.apps = apps
self.http_session = http_session
self.attempts = attempts
self.api_cache = api_cache
self.id_ = '{}#{}'.format(self.__class__.__name__, id(self)) self.id_ = '{}#{}'.format(self.__class__.__name__, id(self))
self.stop = False self.app = app
self.to_persist = {} # stores all data loaded by the instance
self.manager = manager
def log_msg(self, msg: str, color: int = None): def log_msg(self, msg: str, color: int = None):
final_msg = StringIO() final_msg = StringIO()
@@ -41,100 +29,3 @@ class FlatpakAsyncDataLoader(Thread):
final_msg.seek(0) final_msg.seek(0)
print(final_msg.read()) print(final_msg.read())
def run(self):
while True:
if self.apps:
app = self.apps[0]
app.status = ApplicationStatus.LOADING_DATA
for _ in range(0, self.attempts):
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, app.base_data.id), timeout=30)
if res.status_code == 200 and res.text:
data = res.json()
if not app.base_data.version:
app.base_data.version = data.get('version')
if not app.base_data.name:
app.base_data.name = data.get('name')
app.base_data.description = data.get('description', data.get('summary', None))
app.base_data.icon_url = data.get('iconMobileUrl', None)
app.base_data.latest_version = data.get('currentReleaseVersion', app.base_data.version)
if not app.base_data.version and app.base_data.latest_version:
app.base_data.version = app.base_data.latest_version
if app.base_data.icon_url and app.base_data.icon_url.startswith('/'):
app.base_data.icon_url = FLATHUB_URL + app.base_data.icon_url
loaded_data = app.get_data_to_cache()
self.api_cache.add(app.base_data.id, loaded_data)
app.status = ApplicationStatus.READY
if app.supports_disk_cache():
self.to_persist[app.base_data.id] = app
break
else:
self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(app.base_data.id, res.status_code, res.content.decode()), Fore.RED)
except:
self.log_msg("Could not retrieve app data for id '{}'".format(app.base_data.id), Fore.YELLOW)
traceback.print_exc()
if self.apps:
del self.apps[0]
elif self.stop:
self.cache_to_disk()
break # stop working
def add(self, app: FlatpakApplication):
self.apps.append(app)
def current_load(self):
return len(self.apps)
def cache_to_disk(self):
if self.to_persist:
for app in self.to_persist.values():
self.manager.cache_to_disk(app=app, icon_bytes=None, only_icon=False)
self.to_persist = {}
class FlatpakAsyncDataLoaderManager:
def __init__(self, manager: ApplicationManager, api_cache: Cache, worker_load: int = 1, workers: List[FlatpakAsyncDataLoader] = []):
self.worker_load = worker_load
self.current_workers = workers
self.http_session = requests.Session()
self.api_cache = api_cache
self.manager = manager
def load(self, app: FlatpakApplication):
available_workers = [w for w in self.current_workers if w.current_load() < self.worker_load]
if available_workers:
worker = available_workers[0]
else: # new worker
worker = FlatpakAsyncDataLoader(http_session=self.http_session,
api_cache=self.api_cache,
manager=self.manager)
worker.start()
self.current_workers.append(worker)
worker.add(app)
def stop_current_workers(self):
for w in self.current_workers:
w.stop = True
self.current_workers = []

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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=Downgrade
manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to 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.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 manage_window.checkbox.only_apps=Apps
window_manage.input_search.placeholder=Search window_manage.input_search.placeholder=Search
window_manage.input_search.tooltip=Type and press ENTER to search for applications window_manage.input_search.tooltip=Type and press ENTER to search for applications
@@ -22,8 +23,9 @@ manage_window.status.info=Retrieving information
manage_window.status.history=Retrieving history manage_window.status.history=Retrieving history
manage_window.status.searching=Searching manage_window.status.searching=Searching
manage_window.status.installing=Installing manage_window.status.installing=Installing
manage_window.status.refreshing_app=Refreshing
manage_window.bt.refresh.tooltip=Reload the data about installed applications manage_window.bt.refresh.tooltip=Reload the data about installed applications
manage_window.bt.upgrade.tooltip=Upgrade all selected applications manage_window.bt.upgrade.tooltip=Upgrade all checked applications
manage_window.checkbox.show_details=Show details manage_window.checkbox.show_details=Show details
popup.root.title=Requires root privileges popup.root.title=Requires root privileges
popup.root.password=Password popup.root.password=Password
@@ -41,6 +43,11 @@ tray.action.exit=Exit
tray.action.about=About tray.action.about=About
tray.action.refreshing=Refreshing tray.action.refreshing=Refreshing
notification.new_updates=Updates notification.new_updates=Updates
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.arch=arch
flatpak.info.branch=branch flatpak.info.branch=branch
flatpak.info.collection=collection flatpak.info.collection=collection
@@ -60,7 +67,15 @@ flatpak.info.sdk=sdk
flatpak.info.subject=subject flatpak.info.subject=subject
flatpak.info.type=type flatpak.info.type=type
flatpak.info.version=version 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.link=More information at
about.info.license=Free license 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 about.info.rate=If this tool is useful for you, give it a star on Github to keep it up
@@ -74,5 +89,14 @@ latest_version=latest version
description=description description=description
type=type type=type
installed=installed installed=installed
uninstalled=not installed
downgraded=downgraded
others=others others=others
internet.required=Internet connection is required 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

View File

@@ -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=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.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.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.apps_table.upgrade_toggle.tooltip=Haces clic aqui para marcar o desmarcar la actualización
manage_window.checkbox.only_apps=Aplicativos manage_window.checkbox.only_apps=Aplicativos
window_manage.input_search.placeholder=Buscar window_manage.input_search.placeholder=Buscar
@@ -23,8 +24,9 @@ manage_window.status.info=Obteniendo información
manage_window.status.history=Obteniendo la historia manage_window.status.history=Obteniendo la historia
manage_window.status.searching=Buscando manage_window.status.searching=Buscando
manage_window.status.installing=Instalando manage_window.status.installing=Instalando
manage_window.status.refreshing_app=Actualizando
manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados
manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos marcados
manage_window.checkbox.show_details=Mostrar detalles manage_window.checkbox.show_details=Mostrar detalles
popup.root.title=Requiere privilegios de root popup.root.title=Requiere privilegios de root
popup.root.password=Contraseña popup.root.password=Contraseña
@@ -42,6 +44,11 @@ tray.action.exit=Salir
tray.action.about=Sobre tray.action.about=Sobre
tray.action.refreshing=Recargando tray.action.refreshing=Recargando
notification.new_updates=Actualizaciones notification.new_updates=Actualizaciones
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.arch=arquitectura
flatpak.info.branch=rama flatpak.info.branch=rama
flatpak.info.collection=colección flatpak.info.collection=colección
@@ -61,7 +68,15 @@ flatpak.info.sdk=sdk
flatpak.info.subject=tema flatpak.info.subject=tema
flatpak.info.type=tipo flatpak.info.type=tipo
flatpak.info.version=versión 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.link=Mas informaciones en
about.info.license=Licencia gratuita about.info.license=Licencia gratuita
about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla
@@ -75,5 +90,14 @@ latest_version=ultima versión
description=descripción description=descripción
type=tipo type=tipo
installed=instalado installed=instalado
uninstalled=no instalado
downgraded=versión revertida
others=otros others=otros
internet.required=Se requiere conexión a internet 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

View File

@@ -6,11 +6,12 @@ manage_window.apps_table.row.actions.info=Informação
manage_window.apps_table.row.actions.history=Histórico manage_window.apps_table.row.actions.history=Histórico
manage_window.apps_table.row.actions.uninstall=Desinstalar manage_window.apps_table.row.actions.uninstall=Desinstalar
manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação
manage_window.apps_table.row.actions.uninstall.popup.body=Remover {}? 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=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.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ?
manage_window.apps_table.row.actions.install=Instalar 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 manage_window.checkbox.only_apps=Aplicativos
window_manage.input_search.placeholder=Buscar window_manage.input_search.placeholder=Buscar
window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos
@@ -23,8 +24,9 @@ manage_window.status.info=Obtendo informação
manage_window.status.history=Obtendo histórico manage_window.status.history=Obtendo histórico
manage_window.status.searching=Buscando manage_window.status.searching=Buscando
manage_window.status.installing=Instalando manage_window.status.installing=Instalando
manage_window.status.refreshing_app=Atualizando
manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados
manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos marcados
manage_window.checkbox.show_details=Mostrar detalhes manage_window.checkbox.show_details=Mostrar detalhes
popup.root.title=Requer privilégios de root popup.root.title=Requer privilégios de root
popup.root.password=Senha popup.root.password=Senha
@@ -42,6 +44,11 @@ tray.action.exit=Sair
tray.action.about=Sobre tray.action.about=Sobre
tray.action.refreshing=Recarregando tray.action.refreshing=Recarregando
notification.new_updates=Atualizações notification.new_updates=Atualizações
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.arch=arquitetura
flatpak.info.branch=ramo flatpak.info.branch=ramo
flatpak.info.collection=coleção flatpak.info.collection=coleção
@@ -61,7 +68,15 @@ flatpak.info.sdk=sdk
flatpak.info.subject=assunto flatpak.info.subject=assunto
flatpak.info.type=tipo flatpak.info.type=tipo
flatpak.info.version=versão 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.link=Mais informações em
about.info.license=Licença gratuita 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é about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé
@@ -75,5 +90,14 @@ latest_version=última versão
description=descrição description=descrição
type=tipo type=tipo
installed=instalado installed=instalado
uninstalled=não instalado
downgraded=versão revertida
others=outros others=outros
internet.required=É necessário estar conectado a Internet 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

View File

@@ -19,7 +19,8 @@ class Cache:
self.lock.release() self.lock.release()
def _add(self, key: str, val: object): 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): def add_non_existing(self, key: str, val: object):

View File

@@ -3,10 +3,10 @@ from threading import Lock
from typing import List from typing import List
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel QHeaderView, QLabel, QHBoxLayout
from fpakman.core import resource from fpakman.core import resource
from fpakman.core.model import ApplicationStatus 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_on = QIcon(resource.get_path('img/toggle_on.svg'))
self.icon_off = QIcon(resource.get_path('img/toggle_off.svg')) self.icon_off = QIcon(resource.get_path('img/toggle_off.svg'))
self.setIcon(self.icon_on) 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']) self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip'])
if not checked: if not checked:
@@ -35,20 +35,20 @@ class UpdateToggleButton(QToolButton):
def change_state(self, not_checked: bool): def change_state(self, not_checked: bool):
self.app_view.update_checked = not not_checked self.app_view.update_checked = not not_checked
self.setIcon(self.icon_on if not not_checked else self.icon_off) self.setIcon(self.icon_on if not not_checked else self.icon_off)
self.root.change_update_state() self.root.change_update_state(change_filters=False)
class AppsTable(QTableWidget): 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__() super(AppsTable, self).__init__()
self.setParent(parent) self.setParent(parent)
self.window = parent self.window = parent
self.disk_cache = disk_cache self.disk_cache = disk_cache
self.download_icons = download_icons
self.column_names = [parent.locale_keys[key].capitalize() for key in ['name', self.column_names = [parent.locale_keys[key].capitalize() for key in ['name',
'version', 'version',
'latest_version', 'description',
'flatpak.info.description',
'type', 'type',
'installed', 'installed',
'manage_window.columns.update']] 'manage_window.columns.update']]
@@ -56,6 +56,7 @@ class AppsTable(QTableWidget):
self.setFocusPolicy(Qt.NoFocus) self.setFocusPolicy(Qt.NoFocus)
self.setShowGrid(False) self.setShowGrid(False)
self.verticalHeader().setVisible(False) self.verticalHeader().setVisible(False)
self.horizontalHeader().setVisible(False)
self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionBehavior(QTableView.SelectRows)
self.setHorizontalHeaderLabels(self.column_names) self.setHorizontalHeaderLabels(self.column_names)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
@@ -74,13 +75,25 @@ class AppsTable(QTableWidget):
menu_row = QMenu() menu_row = QMenu()
if not app.model.installed and app.model.can_be_installed():
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
action_install.triggered.connect(self._install_app)
menu_row.addAction(action_install)
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.installed:
if app.model.has_info(): if app.model.can_be_refreshed():
action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"]) action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.refresh"])
action_info.setIcon(QIcon(resource.get_path('img/info.svg'))) action_history.setIcon(QIcon(resource.get_path('img/refresh.svg')))
action_info.triggered.connect(self._get_app_info) action_history.triggered.connect(self._refresh_app)
menu_row.addAction(action_info) menu_row.addAction(action_history)
if app.model.has_history(): if app.model.has_history():
action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"]) action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"])
@@ -99,41 +112,32 @@ class AppsTable(QTableWidget):
action_downgrade.triggered.connect(self._downgrade_app) action_downgrade.triggered.connect(self._downgrade_app)
action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg')))
menu_row.addAction(action_downgrade) menu_row.addAction(action_downgrade)
else:
if app.model.can_be_installed():
action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"])
action_install.setIcon(QIcon(resource.get_path('img/install.svg')))
action_install.triggered.connect(self._install_app)
menu_row.addAction(action_install)
menu_row.adjustSize() menu_row.adjustSize()
menu_row.popup(QCursor.pos()) menu_row.popup(QCursor.pos())
menu_row.exec_() menu_row.exec_()
def fill_async_data(self): def fill_async_data(self):
self.lock_async_data.acquire()
if self.window.apps: if self.window.apps:
for idx, app_v in enumerate(self.window.apps): for idx, app_v in enumerate(self.window.apps):
if app_v.visible and app_v.status == ApplicationViewStatus.LOADING and app_v.model.status == ApplicationStatus.READY: 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() app_name = self.item(idx, 0).text()
if not app_name or app_name == '...': if not app_name or app_name == '...':
self.item(idx, 0).setText(app_v.model.base_data.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_version(idx, app_v)
self._set_col_description(self.item(idx, 3), app_v) self._set_col_description(idx, app_v)
app_v.status = ApplicationViewStatus.READY app_v.status = ApplicationViewStatus.READY
self.window.resize_and_center() self.window.resize_and_center()
self.lock_async_data.release()
def get_selected_app(self) -> ApplicationView: def get_selected_app(self) -> ApplicationView:
return self.window.apps[self.currentRow()] return self.window.apps[self.currentRow()]
@@ -156,6 +160,9 @@ class AppsTable(QTableWidget):
locale_keys=self.window.locale_keys): locale_keys=self.window.locale_keys):
self.window.downgrade_app(selected_app) self.window.downgrade_app(selected_app)
def _refresh_app(self):
self.window.refresh(self.get_selected_app())
def _get_app_info(self): def _get_app_info(self):
self.window.get_app_info(self.get_selected_app()) self.window.get_app_info(self.get_selected_app())
@@ -189,49 +196,77 @@ class AppsTable(QTableWidget):
if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()): if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
self.window.manager.cache_to_disk(app=app.model, icon_bytes=icon_data['bytes'], only_icon=True) self.window.manager.cache_to_disk(app=app.model, icon_bytes=icon_data['bytes'], only_icon=True)
def update_apps(self, app_views: List[ApplicationView]): def update_apps(self, app_views: List[ApplicationView], update_check_enabled: bool = True):
self.setEnabled(True)
self.setRowCount(len(app_views) if app_views else 0) self.setRowCount(len(app_views) if app_views else 0)
self.setEnabled(True)
if app_views: if app_views:
for idx, app_v in enumerate(app_views): for idx, app_v in enumerate(app_views):
col_name = self._gen_col_name(app_v) self._set_col_name(idx, app_v)
self.setItem(idx, 0, col_name) 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_version = QLabel(app_v.model.base_data.version) col_update = None
col_version.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 1, col_version)
col_release = QLabel(app_v.get_async_attr('latest_version')) if update_check_enabled and app_v.model.update:
col_release.setAlignment(Qt.AlignCenter) col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update)
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: self.setCellWidget(idx, 5, col_update)
col_release.setStyleSheet("color: orange")
col_description = QTableWidgetItem() def _set_col_installed(self, idx: int, app_v: ApplicationView):
col_description.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) col_installed = QLabel()
self._set_col_description(col_description, app_v)
self.setItem(idx, 3, col_description) 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_type = QLabel(app_v.model.get_type()) col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format(img_name)))))
col_type.setAlignment(Qt.AlignCenter) col_installed.setAlignment(Qt.AlignCenter)
self.setCellWidget(idx, 4, col_type) col_installed.setToolTip(tooltip)
col_installed = QLabel() self.setCellWidget(idx, 4, col_installed)
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) 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)
col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update) if app_v.model.update else None def _set_col_version(self, idx: int, app_v: ApplicationView):
self.setCellWidget(idx, 6, col_update) label_version = QLabel(app_v.model.base_data.version if app_v.model.base_data.version else '?')
label_version.setAlignment(Qt.AlignCenter)
def _gen_col_name(self, app_v: ApplicationView): 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 = QTableWidgetItem()
col.setText(app_v.model.base_data.name if app_v.model.base_data.name else '...') col.setText(app_v.model.base_data.name if app_v.model.base_data.name else '...')
col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 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()): 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: with open(app_v.model.get_disk_icon_path(), 'rb') as f:
@@ -240,6 +275,7 @@ class AppsTable(QTableWidget):
pixmap.loadFromData(icon_bytes) pixmap.loadFromData(icon_bytes)
icon = QIcon(pixmap) icon = QIcon(pixmap)
self.icon_cache.add_non_existing(app_v.model.base_data.icon_url, {'icon': icon, 'bytes': icon_bytes}) 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: elif not app_v.model.base_data.icon_url:
icon = QIcon(app_v.model.get_default_icon_path()) icon = QIcon(app_v.model.get_default_icon_path())
else: else:
@@ -247,9 +283,11 @@ class AppsTable(QTableWidget):
icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path()) icon = icon_data['icon'] if icon_data else QIcon(app_v.model.get_default_icon_path())
col.setIcon(icon) 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) desc = app_v.get_async_attr('description', strip_html=True)
if desc: if desc:
@@ -258,6 +296,8 @@ class AppsTable(QTableWidget):
if app_v.model.status == ApplicationStatus.READY: if app_v.model.status == ApplicationStatus.READY:
col.setToolTip(desc) col.setToolTip(desc)
self.setItem(idx, 2, col)
def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents):
header_horizontal = self.horizontalHeader() header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()): for i in range(self.columnCount()):

View File

@@ -1,5 +1,5 @@
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox, QSizePolicy
from fpakman.core import resource from fpakman.core import resource
@@ -21,6 +21,8 @@ def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIc
dialog_confirmation.setIcon(QMessageBox.Question) dialog_confirmation.setIcon(QMessageBox.Question)
dialog_confirmation.setWindowTitle(title) dialog_confirmation.setWindowTitle(title)
dialog_confirmation.setText(body) dialog_confirmation.setText(body)
dialog_confirmation.setStyleSheet('QLabel { margin-right: 25px; }')
bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole) bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole)
dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole) dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole)

View File

@@ -2,7 +2,7 @@ import operator
from functools import reduce from functools import reduce
from PyQt5.QtCore import Qt 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 from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
@@ -37,7 +37,7 @@ class HistoryDialog(QDialog):
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
if current_app_commit: 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()) tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize())
item.setToolTip(tip) item.setToolTip(tip)

View File

@@ -1,37 +1,47 @@
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \
QLineEdit, QLabel, QPlainTextEdit QLineEdit, QLabel
from fpakman.util import util
class InfoDialog(QDialog): 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__() super(InfoDialog, self).__init__()
self.setWindowTitle(app['name']) self.setWindowTitle(app['name'])
self.setWindowIcon(app_icon) self.setWindowIcon(app_icon)
self.screen_size = screen_size
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) self.setLayout(layout)
gbox_info_layout = QFormLayout()
gbox_info = QGroupBox() 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) gbox_info.setLayout(gbox_info_layout)
layout.addWidget(gbox_info) layout.addWidget(gbox_info)
for attr in sorted(app.keys()): for attr in sorted(app.keys()):
if attr != 'name' and app[attr]: if attr != 'name' and app[attr]:
if attr == 'description': val = app[attr]
text = QPlainTextEdit() text = QLineEdit()
text.appendHtml(app[attr]) text.setToolTip(val)
else:
text = QLineEdit()
text.setText(app[attr])
text.setCursorPosition(0)
text.setStyleSheet("width: 400px")
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) 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") label.setStyleSheet("font-weight: bold")
gbox_info_layout.addRow(label, text) gbox_info_layout.addRow(label, text)

View File

@@ -1,14 +1,15 @@
import time import time
from threading import Lock from threading import Lock, Thread
from typing import List 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.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from fpakman import __app_name__
from fpakman.core import resource, system 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.core.model import ApplicationUpdate
from fpakman.util.cache import Cache from fpakman.util.cache import Cache
from fpakman.view.qt.about import AboutDialog from fpakman.view.qt.about import AboutDialog
from fpakman.view.qt.window import ManageWindow from fpakman.view.qt.window import ManageWindow
@@ -26,25 +27,21 @@ class UpdateCheck(QThread):
def run(self): def run(self):
while True: while True:
updates = self.manager.list_updates()
apps = self.manager.read_installed() self.signal.emit(updates)
updates = [app for app in apps if app.update]
if updates:
self.signal.emit(updates)
time.sleep(self.check_interval) time.sleep(self.check_interval)
class TrayIcon(QSystemTrayIcon): 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__() super(TrayIcon, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager
self.icon_cache = icon_cache self.icon_cache = icon_cache
self.disk_cache = disk_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_default = QIcon(resource.get_path('img/logo.png'))
self.icon_update = QIcon(resource.get_path('img/logo_update.png')) self.icon_update = QIcon(resource.get_path('img/logo_update.png'))
@@ -64,24 +61,38 @@ class TrayIcon(QSystemTrayIcon):
self.setContextMenu(self.menu) self.setContextMenu(self.menu)
self.manage_window = None self.manage_window = None
self.dialog_about = None
self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager) self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager)
self.check_thread.signal.connect(self.notify_updates) self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start() self.check_thread.start()
self.dialog_about = None
self.last_updates = set() self.last_updates = set()
self.update_notification = update_notification self.update_notification = update_notification
self.lock_notify = Lock() self.lock_notify = Lock()
def notify_updates(self, updates: List[Application]): self.activated.connect(self.handle_click)
self.set_default_tooltip()
def set_default_tooltip(self):
self.setToolTip('{} ({})'.format(self.locale_keys['manage_window.title'], __app_name__).lower())
def handle_click(self, reason):
if reason == self.Trigger:
self.show_manage_window()
def verify_updates(self, notify_user: bool = True):
Thread(target=self._verify_updates, args=(notify_user,)).start()
def _verify_updates(self, notify_user: bool):
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
def notify_updates(self, updates: List[ApplicationUpdate], notify_user: bool = True):
self.lock_notify.acquire() self.lock_notify.acquire()
try: try:
if len(updates) > 0: if len(updates) > 0:
update_keys = {'{}:{}:{}'.format(up.type, up.id, up.version) for up in updates}
update_keys = {'{}:{}'.format(app.base_data.id, app.base_data.version) for app in updates}
new_icon = self.icon_update new_icon = self.icon_update
@@ -90,12 +101,13 @@ class TrayIcon(QSystemTrayIcon):
msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], len(updates)) msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], len(updates))
self.setToolTip(msg) self.setToolTip(msg)
if self.update_notification: if self.update_notification and notify_user:
system.notify_user(msg) system.notify_user(msg)
else: else:
self.last_updates.clear()
new_icon = self.icon_default new_icon = self.icon_default
self.setToolTip(None) self.set_default_tooltip()
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
self.setIcon(new_icon) self.setIcon(new_icon)
@@ -104,18 +116,19 @@ class TrayIcon(QSystemTrayIcon):
self.lock_notify.release() self.lock_notify.release()
def show_manage_window(self): def show_manage_window(self):
if self.manage_window is None: if self.manage_window is None:
self.manage_window = ManageWindow(locale_keys=self.locale_keys, self.manage_window = ManageWindow(locale_keys=self.locale_keys,
manager=self.manager, manager=self.manager,
icon_cache=self.icon_cache, icon_cache=self.icon_cache,
tray_icon=self, 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(): if self.manage_window.isMinimized():
self.manage_window.setWindowState(Qt.WindowNoState) self.manage_window.setWindowState(Qt.WindowNoState)
else: elif not self.manage_window.isVisible():
self.manage_window.refresh() self.manage_window.refresh_apps()
self.manage_window.show() self.manage_window.show()
def show_about(self): def show_about(self):

View File

@@ -1,3 +1,4 @@
import subprocess
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List from typing import List
@@ -8,14 +9,52 @@ from PyQt5.QtCore import QThread, pyqtSignal
from fpakman.core.controller import ApplicationManager from fpakman.core.controller import ApplicationManager
from fpakman.core.exception import NoInternetException from fpakman.core.exception import NoInternetException
from fpakman.core.model import ApplicationStatus from fpakman.core.model import ApplicationStatus
from fpakman.core.system import FpakmanProcess
from fpakman.util.cache import Cache from fpakman.util.cache import Cache
from fpakman.view.qt import dialog from fpakman.view.qt import dialog
from fpakman.view.qt.view_model import ApplicationView 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_status = pyqtSignal(str)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None): def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None):
@@ -25,31 +64,20 @@ class UpdateSelectedApps(QThread):
def run(self): def run(self):
error = False success = False
for app in self.apps_to_update: for app in self.apps_to_update:
self.signal_status.emit(app.model.base_data.name)
process = self.manager.update_and_stream(app.model)
success = self.notify_subproc_outputs(process, self.signal_output)
subproc = self.manager.update_and_stream(app.model) if not success:
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:
break 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): class RefreshApps(QThread):
@@ -64,8 +92,8 @@ class RefreshApps(QThread):
self.signal.emit(self.manager.read_installed()) self.signal.emit(self.manager.read_installed())
class UninstallApp(QThread): class UninstallApp(AsyncAction):
signal_finished = pyqtSignal() signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None): def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None):
@@ -73,33 +101,23 @@ class UninstallApp(QThread):
self.app = app self.app = app
self.manager = manager self.manager = manager
self.icon_cache = icon_cache self.icon_cache = icon_cache
self.root_password = None
def run(self): def run(self):
if self.app: if self.app:
subproc = self.manager.uninstall_and_stream(self.app.model) process = self.manager.uninstall_and_stream(self.app.model, self.root_password)
self.signal_output.emit(' '.join(subproc.args) + '\n') success = self.notify_subproc_outputs(process, self.signal_output)
for output in subproc.stdout: if success:
line = output.decode().strip()
if line:
self.signal_output.emit(line)
error = False
for output in subproc.stderr:
line = output.decode().strip()
if line:
error = True
self.signal_output.emit(line)
if not error:
self.icon_cache.delete(self.app.model.base_data.icon_url) self.icon_cache.delete(self.app.model.base_data.icon_url)
self.manager.clean_cache_for(self.app.model) 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_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) signal_output = pyqtSignal(str)
@@ -113,18 +131,15 @@ class DowngradeApp(QThread):
def run(self): def run(self):
if self.app: if self.app:
success = True success = False
try: 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'], dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'],
body=self.locale_keys['popup.downgrade.impossible.body']) body=self.locale_keys['popup.downgrade.impossible.body'])
else: else:
for output in stream: success = self.notify_subproc_outputs(process, self.signal_output)
line = output.decode().strip()
if line:
self.signal_output.emit(line)
except (requests.exceptions.ConnectionError, NoInternetException): except (requests.exceptions.ConnectionError, NoInternetException):
success = False success = False
self.signal_output.emit(self.locale_keys['internet.required']) self.signal_output.emit(self.locale_keys['internet.required'])
@@ -180,54 +195,51 @@ class SearchApps(QThread):
apps_found = [] apps_found = []
if self.word: 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.signal_finished.emit(apps_found)
self.word = None self.word = None
class InstallApp(QThread): class InstallApp(AsyncAction):
signal_finished = pyqtSignal(bool) signal_finished = pyqtSignal(bool)
signal_output = pyqtSignal(str) 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__() super(InstallApp, self).__init__()
self.app = app self.app = app
self.manager = manager self.manager = manager
self.icon_cache = icon_cache self.icon_cache = icon_cache
self.disk_cache = disk_cache self.disk_cache = disk_cache
self.locale_keys = locale_keys
self.root_password = None
def run(self): def run(self):
if self.app: if self.app:
subproc = self.manager.install_and_stream(self.app.model) success = False
self.signal_output.emit(' '.join(subproc.args) + '\n')
for output in subproc.stdout: try:
line = output.decode().strip() process = self.manager.install_and_stream(self.app.model, self.root_password)
if line: success = self.notify_subproc_outputs(process, self.signal_output)
self.signal_output.emit(line)
error = False if success and self.disk_cache:
self.app.model.installed = True
for output in subproc.stderr: if self.app.model.supports_disk_cache():
line = output.decode().strip() icon_data = self.icon_cache.get(self.app.model.base_data.icon_url)
if line: self.manager.cache_to_disk(app=self.app.model,
error = True icon_bytes=icon_data.get('bytes') if icon_data else None,
self.signal_output.emit(line) only_icon=False)
except (requests.exceptions.ConnectionError, NoInternetException):
if not error and self.disk_cache: success = False
self.app.model.installed = True self.signal_output.emit(self.locale_keys['internet.required'])
finally:
if self.app.model.supports_disk_cache(): self.app = None
icon_data = self.icon_cache.get(self.app.model.base_data.icon_url) self.signal_finished.emit(success)
self.manager.cache_to_disk(app=self.app.model,
icon_bytes=icon_data.get('bytes') if icon_data else None,
only_icon=False)
self.app = None
self.signal_finished.emit(not error)
class AnimateProgress(QThread): class AnimateProgress(QThread):
@@ -291,4 +303,33 @@ class VerifyModels(QThread):
if stop_at <= datetime.utcnow(): if stop_at <= datetime.utcnow():
break break
time.sleep(0.1)
self.apps = None 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)

View File

@@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
from fpakman.core.model import Application, ApplicationStatus
from fpakman.util import util from fpakman.util import util
from fpakman.core.model import Application, ApplicationStatus, FlatpakApplication
class ApplicationViewStatus(Enum): class ApplicationViewStatus(Enum):
@@ -19,7 +19,7 @@ class ApplicationView:
def get_async_attr(self, attr: str, strip_html: bool = False, default: str = '...'): 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) res = getattr(self.model.base_data, attr)
else: else:
res = getattr(self.model.base_data, attr) if self.model.status == ApplicationStatus.READY and getattr(self.model.base_data, attr) else default res = getattr(self.model.base_data, attr) if self.model.status == ApplicationStatus.READY and getattr(self.model.base_data, attr) else default

View File

@@ -1,14 +1,13 @@
import operator import operator
from functools import reduce from functools import reduce
from threading import Lock from typing import List, Set
from typing import List
from PyQt5.QtCore import QEvent from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QHBoxLayout
from fpakman.core import resource from fpakman.core import resource, system
from fpakman.core.controller import ApplicationManager from fpakman.core.controller import ApplicationManager
from fpakman.core.model import Application from fpakman.core.model import Application
from fpakman.util.cache import Cache from fpakman.util.cache import Cache
@@ -17,7 +16,7 @@ from fpakman.view.qt.history import HistoryDialog
from fpakman.view.qt.info import InfoDialog from fpakman.view.qt.info import InfoDialog
from fpakman.view.qt.root import is_root, ask_root_password from fpakman.view.qt.root import is_root, ask_root_password
from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp
from fpakman.view.qt.view_model import ApplicationView from fpakman.view.qt.view_model import ApplicationView
DARK_ORANGE = '#FF4500' DARK_ORANGE = '#FF4500'
@@ -26,41 +25,48 @@ DARK_ORANGE = '#FF4500'
class ManageWindow(QWidget): class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400 __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__() super(ManageWindow, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager
self.tray_icon = tray_icon self.tray_icon = tray_icon
self.thread_lock = Lock()
self.working = False # restrict the number of threaded actions self.working = False # restrict the number of threaded actions
self.apps = [] self.apps = []
self.label_flatpak = None self.label_flatpak = None
self.icon_cache = icon_cache self.icon_cache = icon_cache
self.disk_cache = disk_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.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
self.setWindowTitle(locale_keys['manage_window.title'])
self.setWindowIcon(self.icon_flathub) self.setWindowIcon(self.icon_flathub)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setLayout(self.layout) 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 = QToolBar()
self.toolbar_search.setStyleSheet("spacing: 0px;") self.toolbar_search.setStyleSheet("spacing: 0px;")
self.toolbar_search.setContentsMargins(0, 0, 0, 0) self.toolbar_search.setContentsMargins(0, 0, 0, 0)
self.toolbar_search.addWidget(self._new_spacer())
label_pre_search = QLabel() label_pre_search = QLabel()
label_pre_search.setStyleSheet( label_pre_search.setStyleSheet("background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
"background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;")
self.toolbar_search.addWidget(label_pre_search) self.toolbar_search.addWidget(label_pre_search)
self.input_search = QLineEdit() self.input_search = QLineEdit()
self.input_search.setMaxLength(20)
self.input_search.setFrame(False) self.input_search.setFrame(False)
self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder'] + "...") self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder'] + "...")
self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip']) self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip'])
self.input_search.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}") self.input_search.setStyleSheet("QLineEdit { background-color: white; color: gray; spacing: 0;}")
self.input_search.returnPressed.connect(self.search) self.input_search.returnPressed.connect(self.search)
self.toolbar_search.addWidget(self.input_search) self.toolbar_search.addWidget(self.input_search)
@@ -69,30 +75,33 @@ class ManageWindow(QWidget):
label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;") label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;")
self.toolbar_search.addWidget(label_pos_search) self.toolbar_search.addWidget(label_pos_search)
self.toolbar_search.addWidget(self._new_spacer()) self.ref_toolbar_search = self.toolbar_top.addWidget(self.toolbar_search)
self.layout.addWidget(self.toolbar_search) self.toolbar_top.addWidget(self._new_spacer())
self.layout.addWidget(self.toolbar_top)
toolbar = QToolBar() toolbar = QToolBar()
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.checkbox_only_apps = QCheckBox() self.checkbox_only_apps = QCheckBox()
self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps']) self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps'])
self.checkbox_only_apps.setChecked(True) self.checkbox_only_apps.setChecked(True)
self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps) self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps)
toolbar.addWidget(self.checkbox_only_apps) self.ref_checkbox_only_apps = toolbar.addWidget(self.checkbox_only_apps)
toolbar.addWidget(self._new_spacer()) self.extra_filters = QWidget()
self.extra_filters.setLayout(QHBoxLayout())
self.label_status = QLabel() toolbar.addWidget(self.extra_filters)
self.label_status.setText('')
self.label_status.setStyleSheet("font-weight: bold")
toolbar.addWidget(self.label_status)
toolbar.addWidget(self._new_spacer()) toolbar.addWidget(self._new_spacer())
self.bt_refresh = QToolButton() self.bt_refresh = QToolButton()
self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip']) self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip'])
self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
self.bt_refresh.clicked.connect(lambda: self.refresh(keep_console=False)) self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
toolbar.addWidget(self.bt_refresh) toolbar.addWidget(self.bt_refresh)
self.bt_upgrade = QToolButton() self.bt_upgrade = QToolButton()
@@ -100,11 +109,11 @@ class ManageWindow(QWidget):
self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg'))) self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg')))
self.bt_upgrade.setEnabled(False) self.bt_upgrade.setEnabled(False)
self.bt_upgrade.clicked.connect(self.update_selected) self.bt_upgrade.clicked.connect(self.update_selected)
toolbar.addWidget(self.bt_upgrade) self.ref_bt_upgrade = toolbar.addWidget(self.bt_upgrade)
self.layout.addWidget(toolbar) 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.table_apps.change_headers_policy()
self.layout.addWidget(self.table_apps) self.layout.addWidget(self.table_apps)
@@ -130,9 +139,10 @@ class ManageWindow(QWidget):
self.thread_update = UpdateSelectedApps(self.manager) self.thread_update = UpdateSelectedApps(self.manager)
self.thread_update.signal_output.connect(self._update_action_output) self.thread_update.signal_output.connect(self._update_action_output)
self.thread_update.signal_finished.connect(self._finish_update_selected) self.thread_update.signal_finished.connect(self._finish_update_selected)
self.thread_update.signal_status.connect(self._change_updating_app_status)
self.thread_refresh = RefreshApps(self.manager) self.thread_refresh = RefreshApps(self.manager)
self.thread_refresh.signal.connect(self._finish_refresh) self.thread_refresh.signal.connect(self._finish_refresh_apps)
self.thread_uninstall = UninstallApp(self.manager, self.icon_cache) self.thread_uninstall = UninstallApp(self.manager, self.icon_cache)
self.thread_uninstall.signal_output.connect(self._update_action_output) self.thread_uninstall.signal_output.connect(self._update_action_output)
@@ -151,7 +161,7 @@ class ManageWindow(QWidget):
self.thread_search = SearchApps(self.manager) self.thread_search = SearchApps(self.manager)
self.thread_search.signal_finished.connect(self._finish_search) self.thread_search.signal_finished.connect(self._finish_search)
self.thread_install = InstallApp(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_output.connect(self._update_action_output)
self.thread_install.signal_finished.connect(self._finish_install) self.thread_install.signal_finished.connect(self._finish_install)
@@ -161,10 +171,11 @@ class ManageWindow(QWidget):
self.thread_verify_models = VerifyModels() self.thread_verify_models = VerifyModels()
self.thread_verify_models.signal_updates.connect(self._notify_model_data_change) 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.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()) self.toolbar_bottom.addWidget(self._new_spacer())
@@ -174,10 +185,34 @@ class ManageWindow(QWidget):
self.toolbar_bottom.addWidget(self._new_spacer()) 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.layout.addWidget(self.toolbar_bottom)
self.centralize() 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): def _notify_model_data_change(self):
self.table_apps.fill_async_data() self.table_apps.fill_async_data()
@@ -199,25 +234,6 @@ class ManageWindow(QWidget):
self.hide() self.hide()
self._handle_console_option(False) self._handle_console_option(False)
def _acquire_lock(self):
self.thread_lock.acquire()
if not self.working:
self.working = True
self.thread_lock.release()
return self.working
def _release_lock(self):
self.thread_lock.acquire()
if self.working:
self.working = False
self.thread_lock.release()
def _handle_console(self, checked: bool): def _handle_console(self, checked: bool):
if checked: if checked:
@@ -234,65 +250,132 @@ class ManageWindow(QWidget):
self.checkbox_console.setChecked(False) self.checkbox_console.setChecked(False)
self.textarea_output.hide() self.textarea_output.hide()
def refresh(self, keep_console: bool = True): def refresh_apps(self, keep_console: bool = True):
self.filter_types.clear()
self.input_search.clear()
if self._acquire_lock(): if not keep_console:
self.input_search.clear() self._handle_console_option(False)
if not keep_console: self.ref_checkbox_updates.setVisible(False)
self._handle_console_option(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()
self._begin_action(self.locale_keys['manage_window.status.refreshing']) def _finish_refresh_apps(self, apps: List[Application]):
self.thread_refresh.start()
def _finish_refresh(self, apps: List[Application]):
self.update_apps(apps)
self.finish_action() self.finish_action()
self._release_lock() self.ref_checkbox_only_apps.setVisible(True)
self.ref_bt_upgrade.setVisible(True)
self.update_apps(apps)
def uninstall_app(self, app: ApplicationView): def uninstall_app(self, app: ApplicationView):
if self._acquire_lock(): pwd = None
self._handle_console_option(True) requires_root = self.manager.requires_root('uninstall', self.table_apps.get_selected_app().model)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.uninstalling'], app.model.base_data.name))
self.thread_uninstall.app = app if not is_root() and requires_root:
self.thread_uninstall.start() pwd, ok = ask_root_password(self.locale_keys)
def _finish_uninstall(self): if not ok:
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 refresh(self, app: ApplicationView):
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:
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.refreshing_app'], 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.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): def _finish_downgrade(self, success: bool):
self.finish_action() self.finish_action()
self._release_lock()
if success: 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()
if self.tray_icon:
self.tray_icon.verify_updates(notify_user=False)
else: else:
self.checkbox_console.setChecked(True) if self._can_notify_user():
system.notify_user(self.locale_keys['notification.downgrade.failed'])
def filter_only_apps(self, only_apps: int):
if self.apps:
show_only_apps = True if only_apps == 2 else False
for idx, app_v in enumerate(self.apps):
hidden = show_only_apps and app_v.model.is_library()
self.table_apps.setRowHidden(idx, hidden)
app_v.visible = not hidden
self.change_update_state() self.change_update_state()
self.checkbox_console.setChecked(True)
def _finish_refresh(self, success: bool):
self.finish_action()
if success:
self.refresh_apps()
else:
self.change_update_state()
self.checkbox_console.setChecked(True)
def _change_updating_app_status(self, app_name: str):
self.label_status.setText('{} {}...'.format(self.locale_keys['manage_window.status.upgrading'], app_name))
def apply_filters(self):
if self.apps:
visible_apps = len(self.apps)
for idx, app_v in enumerate(self.apps):
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(change_filters=False)
self.table_apps.change_headers_policy(QHeaderView.Stretch) self.table_apps.change_headers_policy(QHeaderView.Stretch)
self.table_apps.change_headers_policy() 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, change_filters: bool = True):
enable_bt_update = False enable_bt_update = False
app_updates, library_updates, not_installed = 0, 0, 0
app_updates, library_updates = 0, 0
for app_v in self.apps: for app_v in self.apps:
if app_v.model.update: if app_v.model.update:
@@ -301,23 +384,40 @@ class ManageWindow(QWidget):
else: else:
app_updates += 1 app_updates += 1
total_updates = app_updates + library_updates if not app_v.model.installed:
if total_updates > 0: not_installed += 1
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: for app_v in self.apps:
if app_v.visible and app_v.update_checked: if not_installed == 0 and app_v.visible and app_v.update_checked:
enable_bt_update = True enable_bt_update = True
break break
self.bt_upgrade.setEnabled(enable_bt_update) self.bt_upgrade.setEnabled(enable_bt_update)
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
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_installed == 0:
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())
def centralize(self): def centralize(self):
geo = self.frameGeometry() geo = self.frameGeometry()
@@ -326,16 +426,16 @@ class ManageWindow(QWidget):
geo.moveCenter(center_point) geo.moveCenter(center_point)
self.move(geo.topLeft()) self.move(geo.topLeft())
def update_apps(self, apps: List[Application]): def update_apps(self, apps: List[Application], update_check_enabled: bool = True):
self.apps = [] self.apps = []
napps = 0 # number of apps (not libraries) napps = 0 # number of apps (not libraries)
available_types = set()
if apps: if apps:
for app in apps: for app in apps:
app_model = ApplicationView(model=app, app_model = ApplicationView(model=app, visible=(not app.is_library()) or not self.checkbox_only_apps.isChecked())
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 napps += 1 if not app.is_library() else 0
self.apps.append(app_model) self.apps.append(app_model)
@@ -346,47 +446,77 @@ class ManageWindow(QWidget):
self.checkbox_only_apps.setCheckable(True) self.checkbox_only_apps.setCheckable(True)
self.checkbox_only_apps.setChecked(True) self.checkbox_only_apps.setChecked(True)
self.table_apps.update_apps(self.apps) self._update_type_filters(available_types)
self.table_apps.update_apps(self.apps, update_check_enabled=update_check_enabled)
self.apply_filters()
self.change_update_state() self.change_update_state()
self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0)
self.resize_and_center() self.resize_and_center()
self.thread_verify_models.apps = self.apps self.thread_verify_models.apps = self.apps
self.thread_verify_models.start() 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.capitalize())
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 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() self.centralize()
def update_selected(self): def update_selected(self):
if self.apps:
if self._acquire_lock(): to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked]
if self.apps:
to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked] if to_update:
self._handle_console_option(True)
if to_update: self._begin_action(self.locale_keys['manage_window.status.upgrading'])
self._handle_console_option(True) self.thread_update.apps_to_update = to_update
self.thread_update.start()
self._begin_action(self.locale_keys['manage_window.status.upgrading']) def _finish_update_selected(self, success: bool, updated: int):
self.thread_update.apps_to_update = to_update
self.thread_update.start()
def _finish_update_selected(self, success: bool):
self.finish_action() self.finish_action()
self._release_lock()
if success: if success:
self.refresh() if self._can_notify_user():
system.notify_user('{} {}'.format(updated, self.locale_keys['notification.update_selected.success']))
self.refresh_apps()
if self.tray_icon:
self.tray_icon.verify_updates()
else: else:
if self._can_notify_user():
system.notify_user(self.locale_keys['notification.update_selected.failed'])
self.bt_upgrade.setEnabled(True) self.bt_upgrade.setEnabled(True)
self.checkbox_console.setChecked(True) self.checkbox_console.setChecked(True)
def _update_action_output(self, output: str): def _update_action_output(self, output: str):
self.textarea_output.appendPlainText(output) self.textarea_output.appendPlainText(output)
def _begin_action(self, action_label: str, 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.ref_label_updates.setVisible(False)
self.thread_animate_progress.stop = False self.thread_animate_progress.stop = False
self.thread_animate_progress.start() self.thread_animate_progress.start()
@@ -397,11 +527,17 @@ class ManageWindow(QWidget):
self.bt_refresh.setEnabled(False) self.bt_refresh.setEnabled(False)
self.checkbox_only_apps.setEnabled(False) self.checkbox_only_apps.setEnabled(False)
self.table_apps.setEnabled(False) self.table_apps.setEnabled(False)
self.checkbox_updates.setEnabled(False)
if keep_search: if keep_search:
self.toolbar_search.setEnabled(False) self.ref_toolbar_search.setVisible(True)
else: 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): def finish_action(self):
self.ref_progress_bar.setVisible(False) self.ref_progress_bar.setVisible(False)
@@ -413,54 +549,49 @@ class ManageWindow(QWidget):
self.table_apps.setEnabled(True) self.table_apps.setEnabled(True)
self.input_search.setEnabled(True) self.input_search.setEnabled(True)
self.label_status.setText('') self.label_status.setText('')
self.toolbar_search.setVisible(True) self.ref_toolbar_search.setVisible(True)
self.toolbar_search.setEnabled(True) self.ref_toolbar_search.setEnabled(True)
self.extra_filters.setEnabled(True)
self.checkbox_updates.setEnabled(True)
def downgrade_app(self, app: ApplicationView): 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)
pwd = None if not is_root() and requires_root:
pwd, ok = ask_root_password(self.locale_keys)
if not is_root(): if not ok:
pwd, ok = ask_root_password(self.locale_keys) return
if not ok: self._handle_console_option(True)
self._release_lock() self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name))
return
self._handle_console_option(True) self.thread_downgrade.app = app
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.downgrading'], app.model.base_data.name)) self.thread_downgrade.root_password = pwd
self.thread_downgrade.start()
self.thread_downgrade.app = app
self.thread_downgrade.root_password = pwd
self.thread_downgrade.start()
def get_app_info(self, app: dict): def get_app_info(self, app: dict):
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.info'])
if self._acquire_lock(): self.thread_get_info.app = app
self._handle_console_option(False) self.thread_get_info.start()
self._begin_action(self.locale_keys['manage_window.status.info'])
self.thread_get_info.app = app
self.thread_get_info.start()
def get_app_history(self, app: dict): def get_app_history(self, app: dict):
if self._acquire_lock(): self._handle_console_option(False)
self._handle_console_option(False) self._begin_action(self.locale_keys['manage_window.status.history'])
self._begin_action(self.locale_keys['manage_window.status.history'])
self.thread_get_history.app = app self.thread_get_history.app = app
self.thread_get_history.start() self.thread_get_history.start()
def _finish_get_info(self, app_info: dict): def _finish_get_info(self, app_info: dict):
self._release_lock()
self.finish_action() self.finish_action()
self.change_update_state() 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_() dialog_info.exec_()
def _finish_get_history(self, app: dict): def _finish_get_history(self, app: dict):
self._release_lock()
self.finish_action() self.finish_action()
self.change_update_state() self.change_update_state()
@@ -476,33 +607,52 @@ class ManageWindow(QWidget):
word = self.input_search.text().strip() word = self.input_search.text().strip()
if word and self._acquire_lock(): if word:
self._handle_console_option(False) 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.word = word
self.thread_search.start() self.thread_search.start()
def _finish_search(self, apps_found: List[Application]): def _finish_search(self, apps_found: List[Application]):
self._release_lock()
self.finish_action() self.finish_action()
self.update_apps(apps_found) self.ref_bt_upgrade.setVisible(False)
self.update_apps(apps_found, update_check_enabled=False)
def install_app(self, app: ApplicationView): def install_app(self, app: ApplicationView):
if self._acquire_lock(): pwd = None
self._handle_console_option(True) requires_root = self.manager.requires_root('install', self.table_apps.get_selected_app().model)
self._begin_action('{} {}'.format(self.locale_keys['manage_window.status.installing'], app.model.base_data.name))
self.thread_install.app = app if not is_root() and requires_root:
self.thread_install.start() pwd, ok = ask_root_password(self.locale_keys)
if not ok:
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): def _finish_install(self, success: bool):
self.input_search.setText('') self.input_search.setText('')
self.finish_action() self.finish_action()
self._release_lock()
if success: 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: 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) self.checkbox_console.setChecked(True)
def _update_progress(self, value: int): def _update_progress(self, value: int):

View File

@@ -1,3 +1,4 @@
pyqt5>=5.12 pyqt5>=5.12
requests>=2.22 requests>=2.22
colorama>=0.4.1 colorama>=0.4.1
beautifulsoup4>=4.7

View File

@@ -2,7 +2,7 @@ import os
from setuptools import setup, find_packages from setuptools import setup, find_packages
DESCRIPTION = ( DESCRIPTION = (
"Graphical user interface to manage Flatpak applications." "Graphical user interface to manage Flatpak / Snap applications."
) )
AUTHOR = "Vinicius Moreira" AUTHOR = "Vinicius Moreira"