diff --git a/CHANGELOG.md b/CHANGELOG.md index affcdc85..da7ba9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.3.0] - 2019-07-02 +### Features +- Applications search +- Now when you right-click a selected application you can: + - retrieve its information + - retrieve its commit history + - downgrade + - install and uninstall it +- "About" window available when right-clicking the tray icon. + +### Improvements +- Performance and memory usage +- Adding tooltips to toolbar buttons +- "Update ?" column renamed to "Upgrade ?" +- Management panel title renamed +- Showing runtime apps when no app is available +- Allowing to specify a custom app translation with the environment variable **FPAKMAN_LOCALE** +- Adding expiration time for cached app data. Default to 1 hour. The environment variable **FPAKMAN_CACHE_EXPIRATION** can change this value. +- Now the application accepts arguments related to environment variables as well. Check 'README.md'. +- Minor GUI improvements +- Notifying only new updates +- New icon +- Progress bar + ## [0.2.1] - 2019-06-24 ### Features - Showing the number of apps and runtime updates available diff --git a/README.md b/README.md index f9895877..d6e35040 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ ## fpakman -Graphical user interface for Flatpak application management. It is a tray icon to let the user known when new updates are available. -It has also a management window allowing the user to see all installed applications and update them. + +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 +an application management panel where you can search, update, install and uninstall applications. ### Developed with: -- Python3 and QT 5. +- Python3 and Qt5. ### Requirements -- libappindicator3 (for GTK3 desktop environments) +- libappindicator3 ( for GTK3 desktop environments ) #### Debian-based distros - python3-venv #### Arch-based distros @@ -23,7 +24,8 @@ sudo pip3 install fpakman ``` **AUR** -As **fpakman** package. There is also a staging version (**fpakman-staging**) but is intended for testing and it may not work properly. + +As **fpakman** package. There is also a staging version (**fpakman-staging**) but is intended for testing and may not work properly. ### Manual installation: @@ -35,14 +37,17 @@ env/bin/fpakman ``` ### Autostart -In order to autostart the application, use your Desktop Environment settings to register it as startup script ("fpakman"). -(P.S: the installation script currently does not do that) +In order to autostart the application, use your Desktop Environment settings to register it as a startup application / script ("fpakman"). + ### Settings -You can change some application settings via environment variables: +You can change some application settings via environment variables or arguments (type ```fpakman --help``` to get more information). - **FPAKMAN_UPDATE_NOTIFICATION**: enable or disable system updates notifications. Use **0** (disable) or **1** (enable, default). - **FPAKMAN_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60. +- **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale. +- **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for application data retrieved from the origin API. Default: 3600 (1 hour). + ### Roadmap -- Search and install applications -- Uninstall applications +- Support for other packaging technologies +- Memory and performance improvements diff --git a/aur/desktop_entry.py b/aur/desktop_entry.py new file mode 100644 index 00000000..080ffa72 --- /dev/null +++ b/aur/desktop_entry.py @@ -0,0 +1,24 @@ +# Generates a .desktop file based on the current python version. Used for AUR installation +import os +import sys +from pathlib import Path + +desktop_file = """ +[Desktop Entry] +Type = Application +Name = fpakman +Categories = System; +Comment = Manage your Flatpak applications +Exec = /usr/bin/fpakman +Icon = /usr/lib/python{version}/site-packages/fpakman/resources/img/logo.svg +""".format(version="{}.{}".format(sys.version_info.major, sys.version_info.minor)) + +with open('fpakman.desktop', 'w+') as f: + f.write(desktop_file) + + +# cleaning the old fpakman.desktop entry model -> the following lines will be removed for the next releases +desktop_file_path = '{}/.local/share/applications/fpakman.desktop'.format(str(Path.home())) + +if os.path.exists(desktop_file_path): + os.remove(desktop_file_path) diff --git a/aur/register_app.py b/aur/register_app.py deleted file mode 100644 index 98516e09..00000000 --- a/aur/register_app.py +++ /dev/null @@ -1,24 +0,0 @@ -# used for AUR installation -import os -import sys -from pathlib import Path - -desktop_file = """ -[Desktop Entry] -Type = Application -Name = fpakman -Categories = System; -Comment = Manage your Flatpak applications -Exec = /usr/bin/fpakman -Icon = /usr/lib/python{version}/site-packages/fpakman/resources/img/flathub_45.svg -""".format(version="{}.{}".format(sys.version_info.major, sys.version_info.minor)) - -apps_path = '{}/.local/share/applications'.format(str(Path.home())) - -if not os.path.exists(apps_path): - os.mkdir(apps_path) - -file_path = '{}/fpakman.desktop'.format(apps_path) - -with open(file_path, 'w+') as f: - f.write(desktop_file) diff --git a/fpakman/__init__.py b/fpakman/__init__.py index fc79d63d..0404d810 100644 --- a/fpakman/__init__.py +++ b/fpakman/__init__.py @@ -1 +1 @@ -__version__ = '0.2.1' +__version__ = '0.3.0' diff --git a/fpakman/app.py b/fpakman/app.py index 0ee4c73c..f23df03b 100755 --- a/fpakman/app.py +++ b/fpakman/app.py @@ -4,29 +4,59 @@ import sys from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication +from colorama import Fore from fpakman import __version__ from fpakman.core import resource from fpakman.core import util -from fpakman.core.controller import FlatpakController -from fpakman.core.model import FlatpakManager +from fpakman.core.controller import FlatpakManager from fpakman.view.qt.systray import TrayIcon -parser = argparse.ArgumentParser(prog='fpakman', description="GUI for Flatpak applications management") +app_name = 'fpakman' + + +def log_msg(msg: str, color: int = None): + + if color is None: + print('[{}] {}'.format(app_name, msg)) + else: + print('{}[{}] {}{}'.format(color, app_name, msg, Fore.RESET)) + + +parser = argparse.ArgumentParser(prog=app_name, description="GUI for Flatpak applications management") parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) +parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached application expiration time in SECONDS. Default: %(default)s') +parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s') +parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s') +parser.add_argument('-n', '--update-notification', action="store", default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enable/disable system notifications for new updates. Default: %(default)s') args = parser.parse_args() -locale_keys = util.get_locale_keys() +if args.cache_exp <= 0: + log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW) + +if not args.locale.strip(): + log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED) + exit(1) + +if args.check_interval <= 0: + log_msg("'check-interval' set as '{}'. It must be >= 0. Aborting...".format(args.check_interval), Fore.RED) + exit(1) + +if args.update_notification == 0: + log_msg('updates notifications are disabled', Fore.YELLOW) + +locale_keys = util.get_locale_keys(args.locale) app = QApplication(sys.argv) -app.setWindowIcon(QIcon(resource.get_path('img/flathub_45.svg'))) -manager = FlatpakManager() -manager.load_database_async() -controller = FlatpakController(manager) +app.setWindowIcon(QIcon(resource.get_path('img/logo.svg'))) + +manager = FlatpakManager(cache_expire=args.cache_exp) trayIcon = TrayIcon(locale_keys=locale_keys, - controller=controller, - check_interval=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60))) + manager=manager, + check_interval=args.check_interval, + update_notification=bool(args.update_notification)) +trayIcon.load_database() trayIcon.show() sys.exit(app.exec_()) diff --git a/fpakman/core/controller.py b/fpakman/core/controller.py index 84ed8233..5e02af2d 100755 --- a/fpakman/core/controller.py +++ b/fpakman/core/controller.py @@ -1,22 +1,134 @@ +from datetime import datetime, timedelta +from threading import Lock from typing import List -from fpakman.core.model import FlatpakManager +import requests + +from fpakman.core import flatpak + +__FLATHUB_URL__ = 'https://flathub.org' +__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' -class FlatpakController: +class FlatpakManager: - def __init__(self, model: FlatpakManager): - self.model = model + def __init__(self, cache_expire: int = 60 * 60): + self.cache_apps = {} + self.cache_expire = cache_expire + self.http_session = requests.Session() + self.lock_db_read = Lock() + self.lock_read = Lock() - def refresh(self) -> List[dict]: - return self.model.read_installed() + def load_full_database(self): - def update(self, app_ref: str): - return self.model.update_app(app_ref) + self.lock_db_read.acquire() - def check_installed(self) -> bool: - version = self.model.get_version() - return False if version is None else version + try: + res = self.http_session.get(__FLATHUB_API_URL__ + '/apps', timeout=30) - def get_version(self) -> str: - return self.model.get_version() + if res.status_code == 200: + for app in res.json(): + self.cache_apps[app['flatpakAppId']] = app + finally: + self.lock_db_read.release() + + def _request_app_data(self, app_id: str): + + try: + res = self.http_session.get('{}/apps/{}'.format(__FLATHUB_API_URL__, app_id), timeout=30) + + if res.status_code == 200: + return res.json() + else: + print("Could not retrieve app data for id '{}'. Server response: {}".format(app_id, res.status_code)) + except: + print("Could not retrieve app data for id '{}'. Timeout".format(app_id)) + return None + + def _fill_api_data(self, app: dict): + + api_data = self.cache_apps.get(app['id']) + + if (not app['runtime'] and not api_data) or (api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()): # if api data is not cached or expired, tries to retrieve it + api_data = self._request_app_data(app['id']) + + if api_data: + + if self.cache_expire > 0: + api_data['expires_at'] = datetime.utcnow() + timedelta(seconds=self.cache_expire) + + self.cache_apps[app['id']] = api_data + + if not api_data: + for attr in ('latest_version', 'icon', 'description'): + if attr not in app: + app[attr] = None + else: + app['latest_version'] = api_data.get('currentReleaseVersion') + app['icon'] = api_data.get('iconMobileUrl') + + for attr in ('name', 'description'): + if not app.get(attr): + app[attr] = api_data.get(attr) + + if app['icon'].startswith('/'): + app['icon'] = __FLATHUB_URL__ + app['icon'] + + def search(self, word: str) -> List[dict]: + + res = [] + apps_found = flatpak.search(word) + + if apps_found: + + already_read = set() + installed_apps = self.read_installed() + + if installed_apps: + for app in apps_found: + for installed_app in installed_apps: + if app['id'] == installed_app['id']: + res.append(installed_app) + already_read.add(app['id']) + + for app in apps_found: + if app['id'] not in already_read: + app['update'] = False + app['installed'] = False + self._fill_api_data(app) + res.append(app) + + return res + + def read_installed(self) -> List[dict]: + + 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() + + for app in installed: + self._fill_api_data(app) + app['update'] = app['id'] in available_updates + app['installed'] = True + + return installed + + finally: + self.lock_read.release() + + def downgrade_app(self, app: dict, 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) diff --git a/fpakman/core/flatpak.py b/fpakman/core/flatpak.py index 82ea400d..5da0358b 100755 --- a/fpakman/core/flatpak.py +++ b/fpakman/core/flatpak.py @@ -1,8 +1,11 @@ import re +import subprocess from typing import List from fpakman.core import system +BASE_CMD = 'flatpak' + def app_str_to_json(line: str, version: str) -> dict: @@ -32,33 +35,53 @@ def app_str_to_json(line: str, version: str) -> dict: else: raise Exception('Unsupported version') - info = re.findall(r'\w+:\s.+', get_app_info(app['id'], app['branch'])) - fields_to_get = ['origin', 'arch', 'ref'] - - for field in info: - field_val = field.split(':') - field_name = field_val[0].lower() - - if field_name in fields_to_get: - app[field_name] = field_val[1].strip() - - if field_name == 'ref': - app['runtime'] = app['ref'].startswith('runtime/') + extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True) + app.update(extra_fields) return app +def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False): + info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch)) + data = {} + fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0) + + for field in info: + + if fields and fields_to_retrieve == 0: + break + + field_val = field.split(':') + field_name = field_val[0].lower() + + if not fields or field_name in fields or (check_runtime and field_name == 'ref'): + data[field_name] = field_val[1].strip() + + if fields: + fields_to_retrieve -= 1 + + if check_runtime and field_name == 'ref': + data['runtime'] = data['ref'].startswith('runtime/') + + return data + + +def is_installed(): + version = get_version() + return False if version is None else True + + def get_version(): - res = system.run_cmd('flatpak --version') + res = system.run_cmd('{} --version'.format(BASE_CMD)) return res.split(' ')[1].strip() if res else None def get_app_info(app_id: str, branch: str): - return system.run_cmd('flatpak info {} {}'.format(app_id, branch)) + return system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch)) def list_installed() -> List[dict]: - apps_str = system.run_cmd('flatpak list') + apps_str = system.run_cmd('{} list'.format(BASE_CMD)) if apps_str: version = get_version() @@ -68,18 +91,123 @@ def list_installed() -> List[dict]: return [] -def update(app_ref: str) -> bool: - return bool(system.run_cmd('flatpak update -y ' + app_ref)) - - def update_and_stream(app_ref: str): """ Updates the app reference and streams Flatpak output, :param app_ref: :return: """ - return system.stream_cmd(['flatpak', 'update', '-y', app_ref]) + return system.stream_cmd([BASE_CMD, 'update', '-y', app_ref]) + + +def uninstall_and_stream(app_ref: str): + """ + Removes the app by its reference + :param app_ref: + :return: + """ + return system.stream_cmd([BASE_CMD, 'uninstall', app_ref, '-y']) def list_updates_as_str(): - return system.run_cmd('flatpak update', 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): + + 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]: + log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) + return re.findall(r'Commit+:\s(.+)', log) + + +def get_app_commits_data(app_ref: str, origin: str) -> List[dict]: + log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) + + res = re.findall(r'(Commit|Subject|Date):\s(.+)', log) + + commits = [] + + commit = {} + + for idx, data in enumerate(res): + commit[data[0].strip().lower()] = data[1].strip() + + if (idx + 1) % 3 == 0: + commits.append(commit) + commit = {} + + return commits + + +def search(word: str) -> List[dict]: + cli_version = get_version() + + res = system.run_cmd('{} search {}'.format(BASE_CMD, word)) + + found = [] + + split_res = res.split('\n') + + if split_res and split_res[0].lower() != 'no matches found': + for info in split_res: + if info: + info_list = info.split('\t') + + if cli_version >= '1.3.0': + version = info_list[3].strip() + found.append({ + 'name': info_list[0].strip(), + 'description': info_list[1].strip(), + 'id': info_list[2].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[4].strip(), + 'origin': info_list[5].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + elif cli_version >= '1.2.0': + desc = info_list[0].split('-') + version = info_list[2].strip() + found.append({ + 'name': desc[0].strip(), + 'description': desc[1].strip(), + 'id': info_list[1].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[3].strip(), + 'origin': info_list[4].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + else: + version = info_list[1].strip() + found.append({ + 'name': '', + 'description': info_list[4].strip(), + 'id': info_list[0].strip(), + 'version': version, + 'latest_version': version, + 'branch': info_list[2].strip(), + 'origin': info_list[3].strip(), + 'runtime': False, + 'arch': None, # unknown at this moment, + 'ref': None # unknown at this moment + }) + return found + + +def install_and_stream(app_id: str, origin: str): + return system.stream_cmd([BASE_CMD, 'install', origin, app_id, '-y']) diff --git a/fpakman/core/model.py b/fpakman/core/model.py deleted file mode 100755 index 8346a4ad..00000000 --- a/fpakman/core/model.py +++ /dev/null @@ -1,108 +0,0 @@ -from threading import Lock, Thread -from typing import List - -import requests -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest - -from fpakman.core import flatpak - -__FLATHUB_URL__ = 'https://flathub.org' -__FLATHUB_API_URL__ = __FLATHUB_URL__ + '/api/v1' - - -class FlatpakManager: - - def __init__(self): - self.apps = [] - self.apps_db = {} - self.http_session = requests.Session() - self.lock_db_read = Lock() - - def load_database_async(self): - Thread(target=self.load_database, daemon=True).start() - - def load_database(self): - - self.lock_db_read.acquire() - - try: - res = self.http_session.get(__FLATHUB_API_URL__ + '/apps') - - if res.status_code == 200: - for app in res.json(): - self.apps_db[app['flatpakAppId']] = app - finally: - self.lock_db_read.release() - - def get_version(self): - return flatpak.get_version() - - def read_installed(self) -> List[dict]: - - installed = flatpak.list_installed() - - if installed: - installed.sort(key=lambda p: p['name'].lower()) - - available_updates = flatpak.list_updates_as_str() - - for app in installed: - - if not self.apps_db: - self.load_database() - - if self.apps_db: - app_data = self.apps_db.get(app['id'], None) - else: - app_data = None - - if not app_data: - app['latest_version'] = None - app['icon'] = None - else: - app['latest_version'] = app_data['currentReleaseVersion'] - app['icon'] = app_data['iconMobileUrl'] - - if app['icon'].startswith('/'): - app['icon'] = __FLATHUB_URL__ + app['icon'] - - app['update'] = app['id'] in available_updates - - self.apps = installed - return [*self.apps] - - def update_apps(self, refs: List[str]) -> List[dict]: - - if self.apps: - - for ref in refs: - package_found = [app for app in self.apps if app['ref'] == ref] - - if package_found: - package_found = package_found[0] - updated = flatpak.update(ref) - - if updated: - package_found['update'] = not updated - - return [*self.apps] - - return [] - - def update_app(self, ref: str): - - """ - :param ref: - :return: the update command stream - """ - - if self.apps: - - package_found = [app for app in self.apps if app['ref'] == ref] - - if package_found: - return flatpak.update_and_stream(ref) - - return None diff --git a/fpakman/core/system.py b/fpakman/core/system.py index ddd5edfa..462f9a66 100644 --- a/fpakman/core/system.py +++ b/fpakman/core/system.py @@ -1,11 +1,18 @@ +import os import subprocess from typing import List +from fpakman.core import resource + def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False) -> str: - res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE) + res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, env={'LANG': 'en'}) return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None def stream_cmd(cmd: List[str]): - return subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout + return subprocess.Popen(cmd, stdout=subprocess.PIPE, env={'LANG': 'en'}).stdout + + +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)) diff --git a/fpakman/core/util.py b/fpakman/core/util.py index f66016ca..678c577d 100644 --- a/fpakman/core/util.py +++ b/fpakman/core/util.py @@ -2,13 +2,20 @@ import locale from fpakman.core import resource import glob +import re + +HTML_RE = re.compile(r'<[^>]+>') -def get_locale_keys(): +def get_locale_keys(key: str = None): - current_locale = locale.getdefaultlocale() locale_path = None + if key is None: + current_locale = locale.getdefaultlocale() + else: + current_locale = [key.strip().lower()] + if current_locale: current_locale = current_locale[0] @@ -34,3 +41,7 @@ def get_locale_keys(): locale_obj[keyval[0].strip()] = keyval[1].strip() return locale_obj + + +def strip_html(string: str): + return HTML_RE.sub('', string) diff --git a/fpakman/resources/img/checked.svg b/fpakman/resources/img/checked.svg new file mode 100755 index 00000000..40ad2bc5 --- /dev/null +++ b/fpakman/resources/img/checked.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/fpakman/resources/img/update.svg b/fpakman/resources/img/downgrade.svg similarity index 83% rename from fpakman/resources/img/update.svg rename to fpakman/resources/img/downgrade.svg index 3dfbb01c..aea4c0e8 100755 --- a/fpakman/resources/img/update.svg +++ b/fpakman/resources/img/downgrade.svg @@ -8,13 +8,13 @@ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" - viewBox="0 0 45 45" + viewBox="0 0 24 24" enable-background="new 0 0 26 26" id="svg8" - sodipodi:docname="update.svg" + sodipodi:docname="downgrade.svg" inkscape:version="0.92.3 (2405546, 2018-03-11)" - width="45" - height="45"> + width="24" + height="24"> @@ -39,13 +39,13 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1366" - inkscape:window-height="739" + inkscape:window-height="703" id="namedview10" showgrid="false" showborder="false" - inkscape:zoom="9.0769231" - inkscape:cx="13" - inkscape:cy="20.813559" + inkscape:zoom="4.5384616" + inkscape:cx="-20.270304" + inkscape:cy="12.13426" inkscape:window-x="0" inkscape:window-y="260" inkscape:window-maximized="1" @@ -56,14 +56,17 @@ fit-margin-bottom="0" /> + transform="matrix(0.92307692,0,0,1,0,-1)" + style="fill:#ff0000"> + inkscape:connector-curvature="0" + style="fill:#ff0000" /> + inkscape:connector-curvature="0" + style="fill:#ff0000" /> diff --git a/fpakman/resources/img/flathub_45.svg b/fpakman/resources/img/flathub.svg similarity index 96% rename from fpakman/resources/img/flathub_45.svg rename to fpakman/resources/img/flathub.svg index 2c72e02e..1b44a5d4 100755 --- a/fpakman/resources/img/flathub_45.svg +++ b/fpakman/resources/img/flathub.svg @@ -9,13 +9,13 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="43.449291" - height="41.737198" - viewBox="0 0 11.495958 11.042967" + width="47.919052" + height="48.110374" + viewBox="0 0 12.678582 12.729203" version="1.1" id="svg3871" - inkscape:version="0.92.4 5da689c313, 2019-01-14" - sodipodi:docname="flathub_logo.svg"> + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="flathub.svg"> + transform="translate(-12.122116,-167.33593)"> + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/info.svg b/fpakman/resources/img/info.svg new file mode 100755 index 00000000..a276e4fb --- /dev/null +++ b/fpakman/resources/img/info.svg @@ -0,0 +1,155 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/install.svg b/fpakman/resources/img/install.svg new file mode 100755 index 00000000..b24ebfbb --- /dev/null +++ b/fpakman/resources/img/install.svg @@ -0,0 +1,120 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/logo.png b/fpakman/resources/img/logo.png new file mode 100644 index 00000000..c557b4c3 Binary files /dev/null and b/fpakman/resources/img/logo.png differ diff --git a/fpakman/resources/img/logo.svg b/fpakman/resources/img/logo.svg new file mode 100755 index 00000000..e480a993 --- /dev/null +++ b/fpakman/resources/img/logo.svg @@ -0,0 +1,477 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Layer 1 \ No newline at end of file diff --git a/fpakman/resources/img/logo_update.png b/fpakman/resources/img/logo_update.png new file mode 100644 index 00000000..6941519b Binary files /dev/null and b/fpakman/resources/img/logo_update.png differ diff --git a/fpakman/resources/img/logo_update.svg b/fpakman/resources/img/logo_update.svg new file mode 100755 index 00000000..2391fa85 --- /dev/null +++ b/fpakman/resources/img/logo_update.svg @@ -0,0 +1,473 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/red_cross.svg b/fpakman/resources/img/red_cross.svg new file mode 100755 index 00000000..28054ffc --- /dev/null +++ b/fpakman/resources/img/red_cross.svg @@ -0,0 +1,115 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/refresh.svg b/fpakman/resources/img/refresh.svg old mode 100644 new mode 100755 index 0158d7d0..16952792 --- a/fpakman/resources/img/refresh.svg +++ b/fpakman/resources/img/refresh.svg @@ -7,22 +7,39 @@ 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" - width="45" - height="44.999996" - viewBox="0 0 44.999999 44.999995" + width="48" + height="48" + viewBox="0 0 47.999999 47.999999" xml:space="preserve" - sodipodi:docname="refresh_blue.svg" - inkscape:version="0.92.4 5da689c313, 2019-01-14">image/svg+xml + transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)" + style="fill:url(#linearGradient542);fill-opacity:1"> + style="fill:url(#linearGradient542);fill-opacity:1"> + style="fill:url(#linearGradient542);fill-opacity:1" /> + style="fill:url(#linearGradient542);fill-opacity:1" /> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> + transform="translate(-16.062155,-439.23)"> \ No newline at end of file diff --git a/fpakman/resources/img/search.svg b/fpakman/resources/img/search.svg new file mode 100755 index 00000000..1519d2f8 --- /dev/null +++ b/fpakman/resources/img/search.svg @@ -0,0 +1,120 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fpakman/resources/img/toggle_off.svg b/fpakman/resources/img/toggle_off.svg index f50edee5..48f4b89a 100755 --- a/fpakman/resources/img/toggle_off.svg +++ b/fpakman/resources/img/toggle_off.svg @@ -9,13 +9,13 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="45" - height="45" - viewBox="0 0 11.90625 11.90625" + width="24" + height="24" + viewBox="0 0 6.35 6.35" version="1.1" id="svg8" inkscape:version="0.92.3 (2405546, 2018-03-11)" - sodipodi:docname="toogle_red.svg"> + sodipodi:docname="toggle_off.svg"> @@ -50,7 +50,7 @@ image/svg+xml - + @@ -58,12 +58,12 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(198.05951,-115.76041)"> + transform="translate(198.05952,-121.31665)"> + cx="-194.88452" + cy="124.49165" + r="3.175" /> diff --git a/fpakman/resources/img/toggle_on.svg b/fpakman/resources/img/toggle_on.svg index 5c341548..4d89b0ca 100755 --- a/fpakman/resources/img/toggle_on.svg +++ b/fpakman/resources/img/toggle_on.svg @@ -7,17 +7,41 @@ 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" - width="45" - height="45" - viewBox="0 0 11.90625 11.90625" + width="24" + height="24" + viewBox="0 0 6.35 6.35" version="1.1" id="svg8" inkscape:version="0.92.3 (2405546, 2018-03-11)" - sodipodi:docname="pak_update_green.svg"> + sodipodi:docname="toggle_on.svg"> + id="defs2"> + + + + + + @@ -58,12 +82,12 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(198.05951,-115.76041)"> + transform="translate(198.05951,-121.31666)"> + cx="-194.88451" + cy="124.49166" + r="3.175" /> diff --git a/fpakman/resources/img/uninstall.svg b/fpakman/resources/img/uninstall.svg new file mode 100755 index 00000000..b228fbcd --- /dev/null +++ b/fpakman/resources/img/uninstall.svg @@ -0,0 +1,96 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/fpakman/resources/img/update_green.svg b/fpakman/resources/img/update_green.svg index 85063166..d4fc7a00 100755 --- a/fpakman/resources/img/update_green.svg +++ b/fpakman/resources/img/update_green.svg @@ -5,16 +5,17 @@ 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" - viewBox="0 0 45 45" + viewBox="0 0 48 48" enable-background="new 0 0 26 26" id="svg8" - sodipodi:docname="update_green.svg" + sodipodi:docname="update_green_2.svg" inkscape:version="0.92.3 (2405546, 2018-03-11)" - width="45" - height="45"> + width="48" + height="48"> @@ -28,7 +29,47 @@ + id="defs12"> + + + + + + + + + transform="matrix(1.8461538,0,0,2,0,-2)" + style="fill:url(#linearGradient311);fill-opacity:1"> + style="fill:url(#linearGradient313);fill-opacity:1" /> + style="fill:url(#linearGradient315);fill-opacity:1" /> + diff --git a/fpakman/resources/locale/en b/fpakman/resources/locale/en index 6a3a38ff..1cb4c76f 100644 --- a/fpakman/resources/locale/en +++ b/fpakman/resources/locale/en @@ -1,17 +1,72 @@ -manage_window.columns.name=Name -manage_window.columns.version=Version +manage_window.title=Flatpak Application Manager manage_window.columns.latest_version=Latest Version -manage_window.columns.branch=Branch -manage_window.columns.arch=Arch -manage_window.columns.ref=Ref -manage_window.columns.origin=Origin -manage_window.columns.update=Update ? +manage_window.columns.installed=Installed +manage_window.columns.update=Upgrade ? +manage_window.apps_table.row.actions.info=Information +manage_window.apps_table.row.actions.history=History +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.title=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} ? +manage_window.apps_table.row.actions.downgrade=Downgrade +manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ? +manage_window.apps_table.row.actions.install=Install +manage_window.apps_table.upgrade_toggle.tooltip=Click here to check or uncheck the update manage_window.checkbox.only_apps=Apps +window_manage.input_search.placeholder=Search +window_manage.input_search.tooltip=Type and press ENTER to search for applications manage_window.label.updates=Updates manage_window.status.refreshing=Refreshing -manage_window.status.updating=Updating +manage_window.status.upgrading=Upgrading +manage_window.status.uninstalling=Uninstalling +manage_window.status.downgrading=Downgrading +manage_window.status.info=Retrieving information +manage_window.status.history=Retrieving history +manage_window.status.searching=Searching +manage_window.status.installing=Installing +manage_window.bt.refresh.tooltip=Reload the data about installed applications +manage_window.bt.upgrade.tooltip=Upgrade all selected applications popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.msg=flatpak seems not to be installed. Exiting +popup.root.title=Requires root privileges +popup.root.password=Password +popup.root.bad_password.title=Error +popup.root.bad_password.body=Wrong password +popup.downgrade.impossible.title=Error +popup.downgrade.impossible.body=Impossible to downgrade: the app is in its first version +popup.history.title=History +popup.history.selected.tooltip=Current version +popup.button.yes=Yes +popup.button.no=No +popup.button.cancel=Cancel tray.action.manage=Manage applications tray.action.exit=Exit -notification.new_updates=Updates found \ No newline at end of file +tray.action.about=About +tray.action.refreshing=Refreshing +notification.new_updates={} updates +flatpak.info.arch=arch +flatpak.info.branch=branch +flatpak.info.collection=collection +flatpak.info.commit=commit +flatpak.info.date=date +flatpak.info.description=description +flatpak.info.id=id +flatpak.info.installation=installation +flatpak.info.installed=installed +flatpak.info.license=license +flatpak.info.name=name +flatpak.info.origin=origin +flatpak.info.parent=parent +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=subject +flatpak.info.type=type +flatpak.info.version=version +about.info.desc=Non-official Flatpak application management graphical interface +about.info.link=More information at +about.info.license=Free license +about.info.rate=If this tool is useful for you, give it a star on Github to keep it up +yes=yes +no=no +version.updated=updated +version.outdated=outdated \ No newline at end of file diff --git a/fpakman/resources/locale/es b/fpakman/resources/locale/es index 801c5b26..19617178 100644 --- a/fpakman/resources/locale/es +++ b/fpakman/resources/locale/es @@ -1,17 +1,72 @@ -manage_window.columns.name=Nombre -manage_window.columns.version=Versión +manage_window.title=Administrador de Aplicativos Flatpak manage_window.columns.latest_version=Ultima Versión -manage_window.columns.branch=Branch -manage_window.columns.arch=Arch -manage_window.columns.ref=Ref -manage_window.columns.origin=Origen -manage_window.columns.update=¿Actualizar? -manage_window.checkbox.only_apps=Apps +manage_window.columns.update=Actualizar ? +manage_window.columns.installed=Instalado +manage_window.apps_table.row.actions.info=Información +manage_window.apps_table.row.actions.history=Historia +manage_window.apps_table.row.actions.uninstall=Desinstalar +manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación +manage_window.apps_table.row.actions.uninstall.popup.body=¿Eliminar {}? +manage_window.apps_table.row.actions.downgrade=Revertir versión +manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quieres revertir la versión actual de {}? +manage_window.apps_table.row.actions.install=Instalar +manage_window.apps_table.upgrade_toggle.tooltip=Haces clic aqui para marcar o desmarcar la actualización +manage_window.checkbox.only_apps=Aplicativos +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Escriba y oprima ENTER para buscar aplicativos manage_window.label.updates=Actualizaciones manage_window.status.refreshing=Recargando -manage_window.status.updating=Actualizando +manage_window.status.upgrading=Actualizando +manage_window.status.uninstalling=Desinstalando +manage_window.status.downgrading=Revirtiendo +manage_window.status.info=Obteniendo información +manage_window.status.history=Obteniendo la historia +manage_window.status.searching=Buscando +manage_window.status.installing=Instalando +manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados +manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos seleccionados popup.flatpak_not_installed.title=Error popup.flatpak_not_installed.msg=flatpak no parece estar instalado. Saliendo +popup.root.title=Requiere privilegios de root +popup.root.password=Contraseña +popup.root.bad_password.title=Error +popup.root.bad_password.body=Contraseña incorrecta +popup.downgrade.impossible.title=Error +popup.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión +popup.history.title=Historia +popup.history.selected.tooltip=Versión actual +popup.button.yes=Sí +popup.button.no=No +popup.button.cancel=Cancelar tray.action.manage=Administrar aplicativos tray.action.exit=Salir -notification.new_updates=Actualizaciones encontradas \ No newline at end of file +tray.action.about=Sobre +tray.action.refreshing=Recargando +notification.new_updates=Actualizaciones {} +flatpak.info.arch=arquitectura +flatpak.info.branch=rama +flatpak.info.collection=colección +flatpak.info.commit=commit +flatpak.info.date=fecha +flatpak.info.description=descripción +flatpak.info.id=id +flatpak.info.installation=instalación +flatpak.info.installed=instalado +flatpak.info.license=licencia +flatpak.info.name=nombre +flatpak.info.origin=origen +flatpak.info.parent=padre +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=tema +flatpak.info.type=tipo +flatpak.info.version=versión +about.info.desc=Interfaz grafica no oficial para administración de aplicativos Flatpak +about.info.link=Mas informaciones en +about.info.license=Licencia gratuita +about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla +yes=sí +no=no +version.updated=actualizada +version.outdated=desactualizada \ No newline at end of file diff --git a/fpakman/resources/locale/pt b/fpakman/resources/locale/pt index 61db2c38..7cddb50c 100644 --- a/fpakman/resources/locale/pt +++ b/fpakman/resources/locale/pt @@ -1,17 +1,72 @@ -manage_window.columns.name=Nome -manage_window.columns.version=Versão +manage_window.title=Gerenciador de Aplicativos Flatpak manage_window.columns.latest_version=Última Versão -manage_window.columns.branch=Branch -manage_window.columns.arch=Arch -manage_window.columns.ref=Ref -manage_window.columns.origin=Origem manage_window.columns.update=Atualizar ? -manage_window.checkbox.only_apps=Apps +manage_window.columns.installed=Instalado +manage_window.apps_table.row.actions.info=Informação +manage_window.apps_table.row.actions.history=Histórico +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.body=Remover {}? +manage_window.apps_table.row.actions.downgrade=Reverter versão +manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ? +manage_window.apps_table.row.actions.install=Instalar +manage_window.apps_table.upgrade_toggle.tooltip=Clique aqui para marcar ou desmarcar a atualização +manage_window.checkbox.only_apps=Aplicativos +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos manage_window.label.updates=Atualizações manage_window.status.refreshing=Recarregando -manage_window.status.updating=Atualizando +manage_window.status.upgrading=Atualizando +manage_window.status.uninstalling=Desinstalando +manage_window.status.downgrading=Revertendo +manage_window.status.info=Obtendo informação +manage_window.status.history=Obtendo histórico +manage_window.status.searching=Buscando +manage_window.status.installing=Instalando +manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados +manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos selecionados popup.flatpak_not_installed.title=Erro popup.flatpak_not_installed.msg=flatpak não parece estar instalado. Saindo +popup.root.title=Requer privilégios de root +popup.root.password=Senha +popup.root.bad_password.title=Erro +popup.root.bad_password.body=Senha incorreta +popup.downgrade.impossible.title=Erro +popup.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão +popup.history.title=Histórico +popup.history.selected.tooltip=Versão atual +popup.button.yes=Sim +popup.button.no=Não +popup.button.cancel=Cancelar tray.action.manage=Gerenciar aplicativos tray.action.exit=Sair -notification.new_updates=Atualizações encontradas \ No newline at end of file +tray.action.about=Sobre +tray.action.refreshing=Recarregando +notification.new_updates=Atualizações {} +flatpak.info.arch=arquitetura +flatpak.info.branch=ramo +flatpak.info.collection=coleção +flatpak.info.commit=commit +flatpak.info.date=data +flatpak.info.description=descrição +flatpak.info.id=id +flatpak.info.installation=instalação +flatpak.info.installed=instalado +flatpak.info.license=licença +flatpak.info.name=nome +flatpak.info.origin=origem +flatpak.info.parent=pai +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=assunto +flatpak.info.type=tipo +flatpak.info.version=versão +about.info.desc=Interface gráfica não oficial para gerenciamento de aplicativos Flatpak +about.info.link=Mais informações em +about.info.license=Licença gratuita +about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé +yes=sim +no=não +version.updated=atualizada +version.outdated=desatualizada \ No newline at end of file diff --git a/fpakman/view/qt/about.py b/fpakman/view/qt/about.py new file mode 100644 index 00000000..b9e319db --- /dev/null +++ b/fpakman/view/qt/about.py @@ -0,0 +1,71 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel + +from fpakman import __version__ +from fpakman.core import resource + +PROJECT_URL = 'https://github.com/vinifmor/fpakman' +LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/fpakman/master/LICENSE' + + +class AboutDialog(QDialog): + + def __init__(self, locale_keys: dict): + super(AboutDialog, self).__init__() + self.setWindowTitle(locale_keys['tray.action.about']) + layout = QVBoxLayout() + self.setLayout(layout) + + label_logo = QLabel(self) + label_logo.setPixmap(QPixmap(resource.get_path('img/logo.svg'))) + label_logo.setAlignment(Qt.AlignCenter) + layout.addWidget(label_logo) + + label_name = QLabel('fpakman ( {} {} )'.format(locale_keys['flatpak.info.version'].lower(), __version__)) + label_name.setStyleSheet('font-weight: bold;') + label_name.setAlignment(Qt.AlignCenter) + layout.addWidget(label_name) + + layout.addWidget(QLabel('')) + + line_desc = QLabel(self) + line_desc.setStyleSheet('font-size: 10px; font-weight: bold;') + line_desc.setText(locale_keys['about.info.desc']) + line_desc.setAlignment(Qt.AlignCenter) + line_desc.setMinimumWidth(400) + layout.addWidget(line_desc) + + layout.addWidget(QLabel('')) + + label_more_info = QLabel() + label_more_info.setStyleSheet('font-size: 9px;') + label_more_info.setText(locale_keys['about.info.link'] + ": {url}".format(url=PROJECT_URL)) + label_more_info.setOpenExternalLinks(True) + label_more_info.setAlignment(Qt.AlignCenter) + layout.addWidget(label_more_info) + + label_license = QLabel() + label_license.setStyleSheet('font-size: 9px;') + label_license.setText("{}".format(LICENSE_URL, locale_keys['about.info.license'])) + label_license.setOpenExternalLinks(True) + label_license.setAlignment(Qt.AlignCenter) + layout.addWidget(label_license) + + layout.addWidget(QLabel('')) + + label_rate = QLabel() + label_rate.setStyleSheet('font-size: 9px; font-weight: bold;') + label_rate.setText(locale_keys['about.info.rate'] + ' :)') + label_rate.setOpenExternalLinks(True) + label_rate.setAlignment(Qt.AlignCenter) + layout.addWidget(label_rate) + + layout.addWidget(QLabel('')) + + self.adjustSize() + self.setFixedSize(self.size()) + + def closeEvent(self, event): + event.ignore() + self.hide() diff --git a/fpakman/view/qt/apps_table.py b/fpakman/view/qt/apps_table.py new file mode 100644 index 00000000..e27a9f4a --- /dev/null +++ b/fpakman/view/qt/apps_table.py @@ -0,0 +1,225 @@ +from typing import List + +from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtGui import QPixmap, QIcon, QColor, QCursor +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager +from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ + QHeaderView, QLabel + +from fpakman.core import resource, util +from fpakman.view.qt import dialog + + +class UpdateToggleButton(QToolButton): + + def __init__(self, model: dict, root: QWidget, locale_keys: dict, checked: bool = True): + super(UpdateToggleButton, self).__init__() + self.app = model + self.root = root + self.setCheckable(True) + self.clicked.connect(self.change_state) + self.icon_on = QIcon(resource.get_path('img/toggle_on.svg')) + self.icon_off = QIcon(resource.get_path('img/toggle_off.svg')) + self.setIcon(self.icon_on) + self.setStyleSheet('border: 0px;') + self.setToolTip(locale_keys['manage_window.apps_table.upgrade_toggle.tooltip']) + + if not checked: + self.click() + + def change_state(self, not_checked: bool): + self.app['update_checked'] = not not_checked + self.setIcon(self.icon_on if not not_checked else self.icon_off) + self.root.change_update_state() + + +class AppsTable(QTableWidget): + + def __init__(self, parent: QWidget): + super(AppsTable, self).__init__() + self.setParent(parent) + self.window = parent + self.column_names = [parent.locale_keys[key].capitalize() for key in ['flatpak.info.name', + 'flatpak.info.version', + 'manage_window.columns.latest_version', + 'flatpak.info.branch', + 'flatpak.info.arch', + 'flatpak.info.id', + 'flatpak.info.origin', + 'manage_window.columns.installed', + 'manage_window.columns.update']] + self.setColumnCount(len(self.column_names)) + self.setFocusPolicy(Qt.NoFocus) + self.setShowGrid(False) + self.verticalHeader().setVisible(False) + self.setSelectionBehavior(QTableView.SelectRows) + self.setHorizontalHeaderLabels(self.column_names) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.icon_flathub = QIcon(resource.get_path('img/flathub.svg')) + + self.network_man = QNetworkAccessManager() + self.network_man.finished.connect(self._load_icon) + + self.icon_cache = {} + + def contextMenuEvent(self, QContextMenuEvent): # selected row right click event + + app = self.get_selected_app() + + menu_row = QMenu() + + if app['model']['installed']: + action_info = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.info"]) + action_info.setIcon(QIcon(resource.get_path('img/info.svg'))) + action_info.triggered.connect(self._get_app_info) + menu_row.addAction(action_info) + + action_history = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.history"]) + action_history.setIcon(QIcon(resource.get_path('img/history.svg'))) + action_history.triggered.connect(self._get_app_history) + menu_row.addAction(action_history) + + action_uninstall = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.uninstall"]) + action_uninstall.setIcon(QIcon(resource.get_path('img/uninstall.svg'))) + action_uninstall.triggered.connect(self._uninstall_app) + menu_row.addAction(action_uninstall) + + if not app['model']['runtime']: # not available for runtimes + action_downgrade = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.downgrade"]) + action_downgrade.triggered.connect(self._downgrade_app) + action_downgrade.setIcon(QIcon(resource.get_path('img/downgrade.svg'))) + menu_row.addAction(action_downgrade) + else: + action_install = QAction(self.window.locale_keys["manage_window.apps_table.row.actions.install"]) + action_install.setIcon(QIcon(resource.get_path('img/install.svg'))) + action_install.triggered.connect(self._install_app) + menu_row.addAction(action_install) + + menu_row.adjustSize() + menu_row.popup(QCursor.pos()) + menu_row.exec_() + + def get_selected_app(self): + return self.window.apps[self.currentRow()] + + def get_selected_app_icon(self): + return self.item(self.currentRow(), 0).icon() + + def _uninstall_app(self): + selected_app = self.get_selected_app() + + if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.title'], + body=self.window.locale_keys['manage_window.apps_table.row.actions.uninstall.popup.body'].format(selected_app['model']['name']), + locale_keys=self.window.locale_keys): + self.window.uninstall_app(selected_app['model']['ref']) + + def _downgrade_app(self): + selected_app = self.get_selected_app() + + if dialog.ask_confirmation(title=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade'], + body=self.window.locale_keys['manage_window.apps_table.row.actions.downgrade.popup.body'].format(selected_app['model']['name']), + locale_keys=self.window.locale_keys): + self.window.downgrade_app(selected_app) + + def _get_app_info(self): + self.window.get_app_info(self.get_selected_app()) + + def _get_app_history(self): + self.window.get_app_history(self.get_selected_app()) + + def _install_app(self): + self.window.install_app(self.get_selected_app()) + + def _load_icon(self, http_response): + icon_url = http_response.url().toString() + pixmap = QPixmap() + pixmap.loadFromData(http_response.readAll()) + icon = QIcon(pixmap) + self.icon_cache[icon_url] = icon + + for idx, app in enumerate(self.window.apps): + if app['model']['icon'] == icon_url: + self.item(idx, 0).setIcon(icon) + self.window.resize_and_center() + break + + def update_apps(self, apps: List[dict]): + self.setEnabled(True) + self.setRowCount(len(apps) if apps else 0) + + if apps: + for idx, app in enumerate(apps): + + tooltip = util.strip_html(app['model']['description']) if app['model']['description'] else None + + col_name = QTableWidgetItem() + col_name.setText(app['model']['name']) + col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_name.setToolTip(tooltip) + + if not app['model']['icon']: + col_name.setIcon(self.icon_flathub) + else: + cached_icon = self.icon_cache.get(app['model']['icon']) + + if cached_icon: + col_name.setIcon(cached_icon) + else: + col_name.setIcon(self.icon_flathub) + self.network_man.get(QNetworkRequest(QUrl(app['model']['icon']))) + + self.setItem(idx, 0, col_name) + + col_version = QTableWidgetItem() + col_version.setText(app['model']['version']) + col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_version.setToolTip(tooltip) + self.setItem(idx, 1, col_version) + + col_release = QTableWidgetItem() + col_release.setText(app['model']['latest_version']) + col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_release.setToolTip(tooltip) + self.setItem(idx, 2, col_release) + + if app['model']['version'] and app['model']['latest_version'] and app['model']['version'] < app['model']['latest_version']: + col_release.setForeground(QColor('orange')) + + col_branch = QTableWidgetItem() + col_branch.setText(app['model']['branch']) + col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_branch.setToolTip(tooltip) + self.setItem(idx, 3, col_branch) + + col_arch = QTableWidgetItem() + col_arch.setText(app['model']['arch']) + col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_arch.setToolTip(tooltip) + self.setItem(idx, 4, col_arch) + + col_id = QTableWidgetItem() + col_id.setText(app['model']['id']) + col_id.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_id.setToolTip(tooltip) + self.setItem(idx, 5, col_id) + + col_origin = QTableWidgetItem() + col_origin.setText(app['model']['origin']) + col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + col_origin.setToolTip(tooltip) + self.setItem(idx, 6, col_origin) + + col_installed = QLabel() + col_installed.setPixmap((QPixmap(resource.get_path('img/{}.svg'.format('checked' if app['model']['installed'] else 'red_cross'))))) + col_installed.setToolTip(tooltip) + col_installed.setAlignment(Qt.AlignCenter) + + self.setCellWidget(idx, 7, col_installed) + + col_update = UpdateToggleButton(app, self.window, self.window.locale_keys, app['model']['update']) if app['model']['update'] else None + self.setCellWidget(idx, 8, col_update) + + def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): + header_horizontal = self.horizontalHeader() + for i in range(self.columnCount()): + header_horizontal.setSectionResizeMode(i, policy) diff --git a/fpakman/view/qt/dialog.py b/fpakman/view/qt/dialog.py new file mode 100644 index 00000000..6b27e6ff --- /dev/null +++ b/fpakman/view/qt/dialog.py @@ -0,0 +1,32 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMessageBox + +from fpakman.core import resource + + +def show_error(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))): + error_msg = QMessageBox() + error_msg.setIcon(QMessageBox.Critical) + error_msg.setWindowTitle(title) + error_msg.setText(body) + + if icon: + error_msg.setWindowIcon(icon) + + error_msg.exec_() + + +def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))): + dialog_confirmation = QMessageBox() + dialog_confirmation.setIcon(QMessageBox.Question) + dialog_confirmation.setWindowTitle(title) + dialog_confirmation.setText(body) + bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole) + dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole) + + if icon: + dialog_confirmation.setWindowIcon(icon) + + dialog_confirmation.exec_() + + return dialog_confirmation.clickedButton() == bt_yes diff --git a/fpakman/view/qt/history.py b/fpakman/view/qt/history.py new file mode 100644 index 00000000..8cb5877f --- /dev/null +++ b/fpakman/view/qt/history.py @@ -0,0 +1,54 @@ +import operator +from functools import reduce + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView + + +class HistoryDialog(QDialog): + + def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict): + super(HistoryDialog, self).__init__() + + self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model']['name'])) + self.setWindowIcon(app_icon) + + layout = QVBoxLayout() + self.setLayout(layout) + + table_history = QTableWidget() + table_history.setFocusPolicy(Qt.NoFocus) + table_history.setShowGrid(False) + table_history.verticalHeader().setVisible(False) + table_history.setAlternatingRowColors(True) + + table_history.setColumnCount(len(app['commits'][0])) + table_history.setRowCount(len(app['commits'])) + table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['commits'][0].keys())]) + + for row, commit in enumerate(app['commits']): + + current_app_commit = app['model']['commit'] == commit['commit'] + + for col, key in enumerate(sorted(commit.keys())): + item = QTableWidgetItem() + item.setText(commit[key]) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + + if current_app_commit: + item.setBackground(Qt.darkYellow if row != 0 else Qt.darkGreen) + tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize()) + + item.setToolTip(tip) + + table_history.setItem(row, col, item) + + layout.addWidget(table_history) + + header_horizontal = table_history.horizontalHeader() + for i in range(0, table_history.columnCount()): + header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch) + + new_width = reduce(operator.add, [table_history.columnWidth(i) for i in range(table_history.columnCount())]) + self.resize(new_width, table_history.height()) diff --git a/fpakman/view/qt/info.py b/fpakman/view/qt/info.py new file mode 100644 index 00000000..0ee09ecf --- /dev/null +++ b/fpakman/view/qt/info.py @@ -0,0 +1,39 @@ +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \ + QLineEdit, QLabel, QPlainTextEdit + + +class InfoDialog(QDialog): + + def __init__(self, app: dict, app_icon: QIcon, locale_keys: dict): + super(InfoDialog, self).__init__() + self.setWindowTitle(app['name']) + self.setWindowIcon(app_icon) + layout = QVBoxLayout() + self.setLayout(layout) + + gbox_info_layout = QFormLayout() + gbox_info = QGroupBox() + gbox_info.setLayout(gbox_info_layout) + layout.addWidget(gbox_info) + + for attr in sorted(app.keys()): + + if attr != 'name' and app[attr]: + if attr == 'description': + text = QPlainTextEdit() + text.appendHtml(app[attr]) + else: + text = QLineEdit() + text.setText(app[attr]) + text.setCursorPosition(0) + text.setStyleSheet("width: 400px") + + text.setReadOnly(True) + + label = QLabel("{}: ".format(locale_keys.get('flatpak.info.' + attr, attr)).capitalize()) + label.setStyleSheet("font-weight: bold") + + gbox_info_layout.addRow(label, text) + + self.adjustSize() diff --git a/fpakman/view/qt/root.py b/fpakman/view/qt/root.py new file mode 100644 index 00000000..55bef81d --- /dev/null +++ b/fpakman/view/qt/root.py @@ -0,0 +1,45 @@ +import io +import os +import subprocess + +from PyQt5.QtWidgets import QInputDialog, QLineEdit + +from fpakman.view.qt.dialog import show_error + + +def is_root(): + return os.getuid() == 0 + + +def ask_root_password(locale_keys: dict): + + dialog_pwd = QInputDialog() + dialog_pwd.setInputMode(QInputDialog.TextInput) + dialog_pwd.setTextEchoMode(QLineEdit.Password) + dialog_pwd.setWindowTitle(locale_keys['popup.root.title']) + dialog_pwd.setLabelText(locale_keys['popup.root.password'] + ':') + dialog_pwd.setCancelButtonText(locale_keys['popup.button.cancel']) + dialog_pwd.resize(400, 200) + + ok = dialog_pwd.exec_() + + if ok: + if not validate_password(dialog_pwd.textValue()): + show_error(title=locale_keys['popup.root.bad_password.title'], + body=locale_keys['popup.root.bad_password.body']) + ok = False + + return dialog_pwd.textValue(), ok + + +def validate_password(password: str) -> bool: + proc = subprocess.Popen('sudo -k && echo {} | sudo -S whoami'.format(password), + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + bufsize=-1) + stream = os._wrap_close(io.TextIOWrapper(proc.stdout), proc) + res = stream.read() + stream.close() + + return bool(res.strip()) diff --git a/fpakman/view/qt/systray.py b/fpakman/view/qt/systray.py index c97a4834..a462531b 100755 --- a/fpakman/view/qt/systray.py +++ b/fpakman/view/qt/systray.py @@ -1,82 +1,139 @@ -import os import time +from typing import List -from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication +from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QSystemTrayIcon, QMenu -from fpakman.core import resource -from fpakman.core.controller import FlatpakController +from fpakman.core import resource, system +from fpakman.core.controller import FlatpakManager +from fpakman.view.qt.about import AboutDialog from fpakman.view.qt.window import ManageWindow class UpdateCheck(QThread): - signal = pyqtSignal(int) + signal = pyqtSignal(list) - def __init__(self, check_interval: int, controller: FlatpakController, parent=None): + def __init__(self, manager: FlatpakManager, check_interval: int, parent=None): super(UpdateCheck, self).__init__(parent) - self.controller = controller self.check_interval = check_interval + self.manager = manager def run(self): while True: - apps = self.controller.refresh() + apps = self.manager.read_installed() - updates = len([app for app in apps if app['update']]) + updates = [app for app in apps if app['update']] - self.signal.emit(updates) + if updates: + self.signal.emit(updates) time.sleep(self.check_interval) +class LoadDatabase(QThread): + + signal_finished = pyqtSignal() + + def __init__(self, manager: FlatpakManager, parent=None): + super(LoadDatabase, self).__init__(parent) + self.manager = manager + + def run(self): + self.manager.load_full_database() + self.signal_finished.emit() + + class TrayIcon(QSystemTrayIcon): - def __init__(self, locale_keys: dict, controller: FlatpakController, check_interval: int = 60): + def __init__(self, locale_keys: dict, manager: FlatpakManager, check_interval: int = 60, update_notification: bool = True): super(TrayIcon, self).__init__() self.locale_keys = locale_keys - self.controller = controller + self.manager = manager - self.icon_default = QIcon(resource.get_path('img/flathub_45.svg')) - self.icon_update = QIcon(resource.get_path('img/update_logo.svg')) + self.icon_default = QIcon(resource.get_path('img/logo.png')) + self.icon_update = QIcon(resource.get_path('img/logo_update.png')) self.setIcon(self.icon_default) self.menu = QMenu() + + self.action_refreshing = self.menu.addAction(self.locale_keys['tray.action.refreshing'] + '...') + self.action_refreshing.setEnabled(False) + self.action_manage = self.menu.addAction(self.locale_keys['tray.action.manage']) self.action_manage.triggered.connect(self.show_manage_window) + self.action_manage.setVisible(False) + + self.action_about = self.menu.addAction(self.locale_keys['tray.action.about']) + self.action_about.triggered.connect(self.show_about) + self.action_exit = self.menu.addAction(self.locale_keys['tray.action.exit']) self.action_exit.triggered.connect(lambda: QCoreApplication.exit()) + self.setContextMenu(self.menu) - self.manage_window = ManageWindow(locale_keys=self.locale_keys, controller=controller, tray_icon=self) - self.check_thread = UpdateCheck(check_interval=check_interval, controller=self.controller) + self.manage_window = None + self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager) self.check_thread.signal.connect(self.notify_updates) self.check_thread.start() - def notify_updates(self, updates: int): - if updates > 0: - if self.icon().cacheKey() != self.icon_update.cacheKey(): - self.setIcon(self.icon_update) + self.dialog_about = None - msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], updates) + self.thread_database = LoadDatabase(manager) + self.thread_database.signal_finished.connect(self._update_menu) + self.last_updates = set() + self.update_notification = update_notification + + def load_database(self): + self.thread_database.start() + + def _update_menu(self): + self.action_refreshing.setVisible(False) + self.action_manage.setVisible(True) + + def notify_updates(self, updates: List[dict]): + + if len(updates) > 0: + + update_keys = {'{}:{}'.format(app['id'], app['latest_version']) for app in updates} + + new_icon = self.icon_update + + if update_keys.difference(self.last_updates): + self.last_updates = update_keys + msg = '{}: {}'.format(self.locale_keys['notification.new_updates'].format('Flatpak'), len(updates)) self.setToolTip(msg) - if bool(os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1)): - os.system("notify-send -i {} '{}'".format(resource.get_path('img/flathub_45.svg'), msg)) - - if self.manage_window: - self.manage_window.refresh() + if self.update_notification: + system.notify_user(msg) else: - self.setIcon(self.icon_default) + new_icon = self.icon_default self.setToolTip(None) + if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed + self.setIcon(new_icon) + def show_manage_window(self): - if not self.manage_window: - self.manage_window = ManageWindow(controller=self.controller) + if self.manage_window is None: + self.manage_window = ManageWindow(locale_keys=self.locale_keys, + manager=self.manager, + tray_icon=self) - self.manage_window.refresh() - self.manage_window.show() + if self.manage_window.isMinimized(): + self.manage_window.setWindowState(Qt.WindowNoState) + else: + self.manage_window.refresh() + self.manage_window.show() + + def show_about(self): + + if self.dialog_about is None: + self.dialog_about = AboutDialog(self.locale_keys) + + if self.dialog_about.isHidden(): + self.dialog_about.show() diff --git a/fpakman/view/qt/thread.py b/fpakman/view/qt/thread.py new file mode 100644 index 00000000..d289ab5c --- /dev/null +++ b/fpakman/view/qt/thread.py @@ -0,0 +1,186 @@ +import time + +from PyQt5.QtCore import QThread, pyqtSignal + +from fpakman.core import flatpak +from fpakman.core.controller import FlatpakManager +from fpakman.view.qt import dialog + + +class UpdateSelectedApps(QThread): + + signal_finished = pyqtSignal() + signal_output = pyqtSignal(str) + + def __init__(self): + super(UpdateSelectedApps, self).__init__() + self.refs_to_update = [] + + def run(self): + + for app_ref in self.refs_to_update: + for output in flatpak.update_and_stream(app_ref): + line = output.decode().strip() + if line: + self.signal_output.emit(line) + + self.signal_finished.emit() + + +class RefreshApps(QThread): + + signal = pyqtSignal(list) + + def __init__(self, manager: FlatpakManager): + super(RefreshApps, self).__init__() + self.manager = manager + + def run(self): + self.signal.emit(self.manager.read_installed()) + + +class UninstallApp(QThread): + signal_finished = pyqtSignal() + signal_output = pyqtSignal(str) + + def __init__(self): + super(UninstallApp, self).__init__() + self.app_ref = None + + def run(self): + if self.app_ref: + for output in flatpak.uninstall_and_stream(self.app_ref): + line = output.decode().strip() + if line: + self.signal_output.emit(line) + + self.signal_finished.emit() + + +class DowngradeApp(QThread): + signal_finished = pyqtSignal() + signal_output = pyqtSignal(str) + + def __init__(self, manager: FlatpakManager, locale_keys: dict): + super(DowngradeApp, self).__init__() + self.manager = manager + self.app = None + self.root_password = None + self.locale_keys = locale_keys + + def run(self): + if self.app: + + stream = self.manager.downgrade_app(self.app['model'], self.root_password) + + if stream is None: + dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], + body=self.locale_keys['popup.downgrade.impossible.body']) + else: + for output in self.manager.downgrade_app(self.app['model'], self.root_password): + line = output.decode().strip() + if line: + self.signal_output.emit(line) + + self.app = None + self.root_password = None + self.signal_finished.emit() + + +class GetAppInfo(QThread): + signal_finished = pyqtSignal(dict) + + def __init__(self): + super(GetAppInfo, self).__init__() + self.app = None + + def run(self): + if self.app: + app_info = flatpak.get_app_info_fields(self.app['model']['id'], self.app['model']['branch']) + app_info['name'] = self.app['model']['name'] + app_info['type'] = 'runtime' if self.app['model']['runtime'] else 'app' + app_info['description'] = self.app['model']['description'] + self.signal_finished.emit(app_info) + self.app = None + + +class GetAppHistory(QThread): + signal_finished = pyqtSignal(dict) + + def __init__(self): + super(GetAppHistory, self).__init__() + self.app = None + + def run(self): + if self.app: + commits = flatpak.get_app_commits_data(self.app['model']['ref'], self.app['model']['origin']) + self.signal_finished.emit({'model': self.app['model'], 'commits': commits}) + self.app = None + + +class SearchApps(QThread): + signal_finished = pyqtSignal(list) + + def __init__(self, manager: FlatpakManager): + super(SearchApps, self).__init__() + self.word = None + self.manager = manager + + def run(self): + apps_found = [] + + if self.word: + apps_found = self.manager.search(self.word) + + self.signal_finished.emit(apps_found) + self.word = None + + +class InstallApp(QThread): + + signal_finished = pyqtSignal() + signal_output = pyqtSignal(str) + + def __init__(self): + super(InstallApp, self).__init__() + self.app = None + + def run(self): + + if self.app: + for output in flatpak.install_and_stream(self.app['model']['id'], self.app['model']['origin']): + line = output.decode().strip() + if line: + self.signal_output.emit(line) + + self.app = None + self.signal_finished.emit() + + +class AnimateProgress(QThread): + + signal_change = pyqtSignal(int) + + def __init__(self): + super(AnimateProgress, self).__init__() + self.progress_value = 0 + self.increment = 5 + self.stop = False + + def run(self): + + current_increment = self.increment + + while not self.stop: + self.signal_change.emit(self.progress_value) + + if self.progress_value == 100: + current_increment = -current_increment + if self.progress_value == 0: + current_increment = self.increment + + self.progress_value += current_increment + + time.sleep(0.05) + + self.progress_value = 0 diff --git a/fpakman/view/qt/window.py b/fpakman/view/qt/window.py index ab5bb616..9a28625c 100755 --- a/fpakman/view/qt/window.py +++ b/fpakman/view/qt/window.py @@ -3,81 +3,84 @@ from functools import reduce from threading import Lock from typing import List -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl, QEvent -from PyQt5.QtGui import QIcon, QColor, QPixmap, QWindowStateChangeEvent -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QTableWidget, \ - QTableWidgetItem, QTableView, QCheckBox, QHeaderView, QToolButton, QToolBar, \ - QSizePolicy, QLabel, QMessageBox, QPlainTextEdit +from PyQt5.QtCore import QEvent +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ + QSizePolicy, QLabel, QPlainTextEdit, QLineEdit, QProgressBar -from fpakman import __version__ -from fpakman.core import resource -from fpakman.core.controller import FlatpakController +from fpakman.core import resource, flatpak +from fpakman.core.controller import FlatpakManager +from fpakman.view.qt import dialog +from fpakman.view.qt.apps_table import AppsTable +from fpakman.view.qt.history import HistoryDialog +from fpakman.view.qt.info import InfoDialog +from fpakman.view.qt.root import is_root, ask_root_password +from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ + GetAppHistory, SearchApps, InstallApp, AnimateProgress - -class UpdateToggleButton(QToolButton): - - def __init__(self, model: dict, root: QWidget, checked: bool = True): - super(UpdateToggleButton, self).__init__() - self.model = model - self.root = root - self.setCheckable(True) - self.clicked.connect(self.change_state) - self.icon_on = QIcon(resource.get_path('img/toggle_on.svg')) - self.icon_off = QIcon(resource.get_path('img/toggle_off.svg')) - self.setIcon(self.icon_on) - self.setStyleSheet('border: 0px;') - - if not checked: - self.click() - - def change_state(self, not_checked: bool): - self.model['update_checked'] = not not_checked - self.setIcon(self.icon_on if not not_checked else self.icon_off) - self.root.change_update_state() +DARK_ORANGE = '#FF4500' class ManageWindow(QWidget): __BASE_HEIGHT__ = 400 - def __init__(self, locale_keys: dict, controller: FlatpakController, tray_icon = None): + def __init__(self, locale_keys: dict, manager: FlatpakManager, tray_icon=None): super(ManageWindow, self).__init__() self.locale_keys = locale_keys - self.column_names = [locale_keys['manage_window.columns.name'], - locale_keys['manage_window.columns.version'], - locale_keys['manage_window.columns.latest_version'], - locale_keys['manage_window.columns.branch'], - locale_keys['manage_window.columns.arch'], - locale_keys['manage_window.columns.ref'], - locale_keys['manage_window.columns.origin'], - locale_keys['manage_window.columns.update']] - self.controller = controller - self.icon_cache = {} + self.manager = manager self.tray_icon = tray_icon self.thread_lock = Lock() self.working = False # restrict the number of threaded actions self.apps = [] self.label_flatpak = None - self.network_man = QNetworkAccessManager() - self.network_man.finished.connect(self._load_icon) - - self.icon_flathub = QIcon(resource.get_path('img/flathub_45.svg')) + self.icon_flathub = QIcon(resource.get_path('img/logo.svg')) self._check_flatpak_installed() self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__) - self.setWindowTitle('fpakman ({})'.format(__version__)) + self.setWindowTitle(locale_keys['manage_window.title']) self.setWindowIcon(self.icon_flathub) self.layout = QVBoxLayout() self.setLayout(self.layout) + self.toolbar_search = QToolBar() + self.toolbar_search.setStyleSheet("spacing: 0px;") + self.toolbar_search.setContentsMargins(0, 0, 0, 0) + + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self.toolbar_search.addWidget(spacer) + + label_pre_search = QLabel() + label_pre_search.setStyleSheet("background: white; border-top-left-radius: 5px; border-bottom-left-radius: 5px;") + self.toolbar_search.addWidget(label_pre_search) + + self.input_search = QLineEdit() + self.input_search.setFrame(False) + self.input_search.setPlaceholderText(self.locale_keys['window_manage.input_search.placeholder']+"...") + self.input_search.setToolTip(self.locale_keys['window_manage.input_search.tooltip']) + self.input_search.setStyleSheet("QLineEdit { background-color: white; color: grey; spacing: 0;}") + self.input_search.returnPressed.connect(self.search) + self.toolbar_search.addWidget(self.input_search) + + label_pos_search = QLabel() + label_pos_search.setPixmap(QPixmap(resource.get_path('img/search.svg'))) + label_pos_search.setStyleSheet("background: white; padding-right: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px;") + self.toolbar_search.addWidget(label_pos_search) + + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar_search.addWidget(spacer) + self.layout.addWidget(self.toolbar_search) + + toolbar = QToolBar() + self.checkbox_only_apps = QCheckBox() self.checkbox_only_apps.setText(self.locale_keys['manage_window.checkbox.only_apps']) self.checkbox_only_apps.setChecked(True) self.checkbox_only_apps.stateChanged.connect(self.filter_only_apps) - - toolbar = QToolBar() toolbar.addWidget(self.checkbox_only_apps) spacer = QWidget() @@ -86,7 +89,7 @@ class ManageWindow(QWidget): self.label_status = QLabel() self.label_status.setText('') - self.label_status.setStyleSheet("color: orange") + self.label_status.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) toolbar.addWidget(self.label_status) spacer = QWidget() @@ -94,28 +97,22 @@ class ManageWindow(QWidget): toolbar.addWidget(spacer) self.bt_refresh = QToolButton() + self.bt_refresh.setToolTip(locale_keys['manage_window.bt.refresh.tooltip']) self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.clicked.connect(lambda: self.refresh(clear_output=True)) toolbar.addWidget(self.bt_refresh) - self.bt_update = QToolButton() - self.bt_update.setIcon(QIcon(resource.get_path('img/update_green.svg'))) - self.bt_update.setEnabled(False) - self.bt_update.clicked.connect(self.update_selected) - toolbar.addWidget(self.bt_update) + self.bt_upgrade = QToolButton() + self.bt_upgrade.setToolTip(locale_keys['manage_window.bt.upgrade.tooltip']) + self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg'))) + self.bt_upgrade.setEnabled(False) + self.bt_upgrade.clicked.connect(self.update_selected) + toolbar.addWidget(self.bt_upgrade) self.layout.addWidget(toolbar) - self.table_apps = QTableWidget() - self.table_apps.setColumnCount(len(self.column_names)) - self.table_apps.setFocusPolicy(Qt.NoFocus) - self.table_apps.setShowGrid(False) - self.table_apps.verticalHeader().setVisible(False) - self.table_apps.setSelectionBehavior(QTableView.SelectRows) - self.table_apps.setHorizontalHeaderLabels(self.column_names) - self.table_apps.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - - self._change_table_headers_policy() + self.table_apps = AppsTable(self) + self.table_apps.change_headers_policy() self.layout.addWidget(self.table_apps) @@ -126,35 +123,66 @@ class ManageWindow(QWidget): self.textarea_output.setVisible(False) self.textarea_output.setReadOnly(True) - self.thread_update = UpdateSelectedApps(self.controller) + self.thread_update = UpdateSelectedApps() self.thread_update.signal_output.connect(self._update_action_output) self.thread_update.signal_finished.connect(self._finish_update_selected) - self.thread_refresh = RefreshApps(self.controller) + self.thread_refresh = RefreshApps(self.manager) self.thread_refresh.signal.connect(self._finish_refresh) + self.thread_uninstall = UninstallApp() + self.thread_uninstall.signal_output.connect(self._update_action_output) + self.thread_uninstall.signal_finished.connect(self._finish_uninstall) + + self.thread_downgrade = DowngradeApp(self.manager, self.locale_keys) + self.thread_downgrade.signal_output.connect(self._update_action_output) + self.thread_downgrade.signal_finished.connect(self._finish_downgrade) + + self.thread_get_info = GetAppInfo() + self.thread_get_info.signal_finished.connect(self._finish_get_info) + + self.thread_get_history = GetAppHistory() + self.thread_get_history.signal_finished.connect(self._finish_get_history) + + self.thread_search = SearchApps(self.manager) + self.thread_search.signal_finished.connect(self._finish_search) + + self.thread_install = InstallApp() + self.thread_install.signal_output.connect(self._update_action_output) + self.thread_install.signal_finished.connect(self._finish_install) + + self.thread_animate_progress = AnimateProgress() + self.thread_animate_progress.signal_change.connect(self._update_progress) + self.toolbar_bottom = QToolBar() self.label_updates = QLabel('') + self.label_updates.setStyleSheet("color: {}; font-weight: bold".format(DARK_ORANGE)) self.toolbar_bottom.addWidget(self.label_updates) + + spacer = QWidget() + spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.toolbar_bottom.addWidget(spacer) + + self.progress_bar = QProgressBar() + self.progress_bar.setTextVisible(False) + self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar) + spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.toolbar_bottom.addWidget(spacer) self.label_flatpak = QLabel(self._get_flatpak_label()) self.toolbar_bottom.addWidget(self.label_flatpak) + self.layout.addWidget(self.toolbar_bottom) self.centralize() - def _change_table_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents): - header_horizontal = self.table_apps.horizontalHeader() - for i in range(0, len(self.column_names)): - header_horizontal.setSectionResizeMode(i, policy) - def changeEvent(self, e: QEvent): if isinstance(e, QWindowStateChangeEvent): - self._change_table_headers_policy(QHeaderView.Stretch if self.isMaximized() else QHeaderView.ResizeToContents) + policy = QHeaderView.Stretch if self.isMaximized() else QHeaderView.ResizeToContents + self.table_apps.change_headers_policy(policy) def closeEvent(self, event): @@ -162,35 +190,19 @@ class ManageWindow(QWidget): event.ignore() self.hide() - def _load_icon(self, http_response): - icon_url = http_response.url().toString() - pixmap = QPixmap() - pixmap.loadFromData(http_response.readAll()) - icon = QIcon(pixmap) - self.icon_cache[icon_url] = icon - - for idx, app in enumerate(self.apps): - if app['model']['icon'] == icon_url: - self.table_apps.item(idx, 0).setIcon(icon) - self.resize_and_center() - break - def _check_flatpak_installed(self): - if not self.controller.check_installed(): - error_msg = QMessageBox() - error_msg.setIcon(QMessageBox.Critical) - error_msg.setWindowTitle(self.locale_keys['popup.flatpak_not_installed.title']) - error_msg.setText(self.locale_keys['popup.flatpak_not_installed.msg'] + '...') - error_msg.setWindowIcon(self.icon_flathub) - error_msg.exec_() + if not flatpak.is_installed(): + dialog.show_error(title=self.locale_keys['popup.flatpak_not_installed.title'], + body=self.locale_keys['popup.flatpak_not_installed.msg'] + '...', + icon=self.icon_flathub) exit(1) if self.label_flatpak: self.label_flatpak.setText(self._get_flatpak_label()) def _get_flatpak_label(self): - return 'flatpak: ' + self.controller.get_version() + return 'flatpak: ' + flatpak.get_version() def _acquire_lock(self): @@ -211,24 +223,52 @@ class ManageWindow(QWidget): self.thread_lock.release() + def _hide_output(self): + self.textarea_output.clear() + self.textarea_output.hide() + + def _show_output(self): + self.textarea_output.clear() + self.textarea_output.show() + def refresh(self, clear_output: bool = True): if self._acquire_lock(): self._check_flatpak_installed() - self._begin_action(self.locale_keys['manage_window.status.refreshing'] + '...') + self._begin_action(self.locale_keys['manage_window.status.refreshing']) if clear_output: - self.textarea_output.clear() - self.textarea_output.hide() + self._hide_output() self.thread_refresh.start() - def _finish_refresh(self): + def _finish_refresh(self, apps: List[dict]): - self.update_apps(self.thread_refresh.apps) + self.update_apps(apps) self.finish_action() self._release_lock() + def uninstall_app(self, app_ref: str): + self._check_flatpak_installed() + + if self._acquire_lock(): + self.textarea_output.clear() + self.textarea_output.setVisible(True) + self._begin_action(self.locale_keys['manage_window.status.uninstalling']) + + self.thread_uninstall.app_ref = app_ref + self.thread_uninstall.start() + + def _finish_uninstall(self): + self.finish_action() + self._release_lock() + self.refresh(clear_output=False) + + def _finish_downgrade(self): + self.finish_action() + self._release_lock() + self.refresh(clear_output=False) + def filter_only_apps(self, only_apps: int): if self.apps: @@ -240,8 +280,8 @@ class ManageWindow(QWidget): app['visible'] = not hidden self.change_update_state() - self._change_table_headers_policy(QHeaderView.Stretch) - self._change_table_headers_policy() + self.table_apps.change_headers_policy(QHeaderView.Stretch) + self.table_apps.change_headers_policy() self.resize_and_center() def change_update_state(self): @@ -259,7 +299,10 @@ class ManageWindow(QWidget): total_updates = app_updates + runtime_updates if total_updates > 0: - self.label_updates.setText('{}: {} ( {} apps | {} runtimes )'.format(self.locale_keys['manage_window.label.updates'], total_updates, app_updates, runtime_updates)) + self.label_updates.setText('{}: {}'.format(self.locale_keys['manage_window.label.updates'], total_updates)) + self.label_updates.setToolTip('{} {} | {} runtimes'.format(app_updates, + self.locale_keys['manage_window.checkbox.only_apps'].lower(), + runtime_updates)) else: self.label_updates.setText('') @@ -268,9 +311,8 @@ class ManageWindow(QWidget): enable_bt_update = True break - self.bt_update.setEnabled(enable_bt_update) - - self.tray_icon.notify_updates(total_updates) + self.bt_upgrade.setEnabled(enable_bt_update) + self.tray_icon.notify_updates([app['model'] for app in self.apps if app['model']['update']]) def centralize(self): geo = self.frameGeometry() @@ -282,79 +324,33 @@ class ManageWindow(QWidget): def update_apps(self, apps: List[dict]): self._check_flatpak_installed() - self.table_apps.setEnabled(True) self.apps = [] - self.table_apps.setRowCount(len(apps) if apps else 0) + napps = 0 # number of apps (not runtimes) if apps: - for idx, app in enumerate(apps): - - col_name = QTableWidgetItem() - col_name.setText(app['name']) - col_name.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - - if not app['icon']: - col_name.setIcon(self.icon_flathub) - else: - cached_icon = self.icon_cache.get(app['icon']) - - if cached_icon: - col_name.setIcon(cached_icon) - else: - col_name.setIcon(self.icon_flathub) - self.network_man.get(QNetworkRequest(QUrl(app['icon']))) - - self.table_apps.setItem(idx, 0, col_name) - - col_version = QTableWidgetItem() - col_version.setText(app['version']) - col_version.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 1, col_version) - - col_release = QTableWidgetItem() - col_release.setText(app['latest_version']) - col_release.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 2, col_release) - - if app['update']: - col_release.setForeground(QColor('orange')) - - col_branch = QTableWidgetItem() - col_branch.setText(app['branch']) - col_branch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 3, col_branch) - - col_arch = QTableWidgetItem() - col_arch.setText(app['arch']) - col_arch.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 4, col_arch) - - col_package = QTableWidgetItem() - col_package.setText(app['ref']) - col_package.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 5, col_package) - - col_origin = QTableWidgetItem() - col_origin.setText(app['origin']) - col_origin.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - self.table_apps.setItem(idx, 6, col_origin) - + for app in apps: app_model = {'model': app, 'update_checked': app['update'], 'visible': not app['runtime'] or not self.checkbox_only_apps.isChecked()} - col_update = UpdateToggleButton(app_model, self, app['update']) if app['update'] else None - self.table_apps.setCellWidget(idx, 7, col_update) - + napps += 1 if not app['runtime'] else 0 self.apps.append(app_model) + if napps == 0: + self.checkbox_only_apps.setChecked(False) + self.checkbox_only_apps.setCheckable(False) + else: + self.checkbox_only_apps.setCheckable(True) + self.checkbox_only_apps.setChecked(True) + + self.table_apps.update_apps(self.apps) self.change_update_state() self.filter_only_apps(2 if self.checkbox_only_apps.isChecked() else 0) self.resize_and_center() def resize_and_center(self): - new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.column_names))]) * 1.05 + new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(len(self.table_apps.column_names))]) * 1.05 self.resize(new_width, self.height()) self.centralize() @@ -369,7 +365,7 @@ class ManageWindow(QWidget): self.textarea_output.clear() self.textarea_output.setVisible(True) - self._begin_action(self.locale_keys['manage_window.status.updating'] + '...') + self._begin_action(self.locale_keys['manage_window.status.upgrading']) self.thread_update.refs_to_update = to_update self.thread_update.start() @@ -382,50 +378,121 @@ class ManageWindow(QWidget): self.textarea_output.appendPlainText(output) def _begin_action(self, action_label: str): - self.label_status.setText(action_label) - self.bt_update.setEnabled(False) + self.thread_animate_progress.stop = False + self.thread_animate_progress.start() + self.ref_progress_bar.setVisible(True) + self.progress_bar.setValue(50) + self.label_status.setText(action_label + "...") + self.toolbar_search.setVisible(False) + self.bt_upgrade.setEnabled(False) self.bt_refresh.setEnabled(False) self.checkbox_only_apps.setEnabled(False) self.table_apps.setEnabled(False) - def finish_action(self): + def finish_action(self, clear_search: bool = True): + self.thread_animate_progress.stop = True + self.ref_progress_bar.setVisible(False) + self.progress_bar.setValue(0) + self.progress_bar.setVisible(False) self.bt_refresh.setEnabled(True) + self.toolbar_search.setVisible(True) self.checkbox_only_apps.setEnabled(True) self.table_apps.setEnabled(True) + self.input_search.setEnabled(True) self.label_status.setText('') + if clear_search: + self.input_search.setText('') -# Threaded actions -class UpdateSelectedApps(QThread): + def downgrade_app(self, app: dict): - signal_finished = pyqtSignal() - signal_output = pyqtSignal(str) + self._check_flatpak_installed() - def __init__(self, controller: FlatpakController): - super(UpdateSelectedApps, self).__init__() - self.controller = controller - self.refs_to_update = [] + if self._acquire_lock(): - def run(self): + pwd = None - for app_ref in self.refs_to_update: - for output in self.controller.update(app_ref): - line = output.decode().strip() - if line: - self.signal_output.emit(line) + if not is_root(): + pwd, ok = ask_root_password(self.locale_keys) - self.signal_finished.emit() + if not ok: + self._release_lock() + return + self.textarea_output.clear() + self.textarea_output.setVisible(True) + self._begin_action(self.locale_keys['manage_window.status.downgrading']) -class RefreshApps(QThread): + self.thread_downgrade.app = app + self.thread_downgrade.root_password = pwd + self.thread_downgrade.start() - signal = pyqtSignal() + def get_app_info(self, app: dict): - def __init__(self, controller: FlatpakController): - super(RefreshApps, self).__init__() - self.controller = controller - self.apps = None + if self._acquire_lock(): + self.textarea_output.clear() + self.textarea_output.setVisible(False) + self._begin_action(self.locale_keys['manage_window.status.info']) - def run(self): - self.apps = self.controller.refresh() - self.signal.emit() + self.thread_get_info.app = app + self.thread_get_info.start() + + def get_app_history(self, app: dict): + if self._acquire_lock(): + self.textarea_output.clear() + self.textarea_output.setVisible(False) + self._begin_action(self.locale_keys['manage_window.status.history']) + + self.thread_get_history.app = app + self.thread_get_history.start() + + def _finish_get_info(self, app_info: dict): + self._release_lock() + self.finish_action() + self.change_update_state() + dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys) + dialog_info.exec_() + + def _finish_get_history(self, app: dict): + self._release_lock() + self.finish_action() + self.change_update_state() + dialog_history = HistoryDialog(app, self.table_apps.get_selected_app_icon(), self.locale_keys) + dialog_history.exec_() + + def search(self): + + word = self.input_search.text().strip() + + if word and self._acquire_lock(): + self.textarea_output.clear() + self.textarea_output.setVisible(False) + self._begin_action(self.locale_keys['manage_window.status.searching']) + self.thread_search.word = word + self.thread_search.start() + + def _finish_search(self, apps_found: List[dict]): + + self._release_lock() + self.finish_action(clear_search=False) + self.update_apps(apps_found) + + def install_app(self, app: dict): + + self._check_flatpak_installed() + + if self._acquire_lock(): + self._begin_action(self.locale_keys['manage_window.status.installing']) + self._show_output() + + self.thread_install.app = app + self.thread_install.start() + + def _finish_install(self): + self.input_search.setText('') + self.finish_action() + self._release_lock() + self.refresh(clear_output=False) + + def _update_progress(self, value: int): + self.progress_bar.setValue(value) diff --git a/requirements.txt b/requirements.txt index 9ef4c21f..3415a156 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyqt5>=5.12 -requests>=2.22 \ No newline at end of file +requests>=2.22 +colorama>=0.4.1 \ No newline at end of file