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 @@
+
+
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)">
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
+
+
\ 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 @@
+
+
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