### 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
This commit is contained in:
Vinícius Moreira
2019-07-02 10:53:04 -03:00
committed by GitHub
parent 7ec62618fa
commit 2d837804e3
41 changed files with 3614 additions and 536 deletions

View File

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

View File

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

24
aur/desktop_entry.py Normal file
View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = '0.2.1'
__version__ = '0.3.0'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
viewBox="0 0 11.999997 12"
enable-background="new 0 0 26 26"
id="svg817"
sodipodi:docname="checked.svg"
width="11.999997"
height="12"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata823">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs821" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview819"
showgrid="false"
showborder="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="4.7692308"
inkscape:cx="-42.8741"
inkscape:cy="25.96043"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="svg817" />
<path
d="M 0.13846154,6.6 C 0.04615385,6.48 0,6.3 0,6.18 0,6.06 0.04615385,5.88 0.13846154,5.76 L 0.78461536,4.92 c 0.1846154,-0.24 0.46153844,-0.24 0.64615384,0 l 0.046153,0.06 2.5384614,3.54 c 0.092308,0.12 0.2307693,0.12 0.323077,0 L 10.523077,0.18 h 0.04615 v 0 c 0.184616,-0.24 0.461539,-0.24 0.646154,0 l 0.646154,0.84 c 0.184616,0.24 0.184616,0.6 0,0.84 v 0 L 4.4769229,11.82 C 4.3846154,11.94 4.2923076,12 4.1538462,12 4.0153844,12 3.923077,11.94 3.8307691,11.82 L 0.23076922,6.78 Z"
id="path815"
inkscape:connector-curvature="0"
style="fill:#00ff00;stroke-width:0.52623481" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -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">
<metadata
id="metadata14">
<rdf:RDF>
@@ -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" />
<g
id="g6"
transform="matrix(1.7307692,0,0,1.875,0,-1.875)">
transform="matrix(0.92307692,0,0,1,0,-1)"
style="fill:#ff0000">
<path
d="m 25,17 h -2 c -0.6,0 -1,0.4 -1,1 v 2.5 C 22,20.8 21.8,21 21.5,21 H 4.5 C 4.2,21 4,20.8 4,20.5 V 18 C 4,17.4 3.6,17 3,17 H 1 c -0.6,0 -1,0.4 -1,1 v 6 c 0,0.6 0.4,1 1,1 h 24 c 0.6,0 1,-0.4 1,-1 v -6 c 0,-0.6 -0.4,-1 -1,-1 z"
id="path2"
inkscape:connector-curvature="0" />
inkscape:connector-curvature="0"
style="fill:#ff0000" />
<path
d="m 12.3,16.7 c 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 l 6,-6 C 19.9,10.5 20,10.3 20,10 20,9.7 19.9,9.5 19.7,9.3 L 18.3,7.9 C 18.1,7.7 17.9,7.6 17.6,7.6 c -0.3,0 -0.5,0.1 -0.7,0.3 l -1,1 C 15.6,9.2 15,9 15,8.5 V 2 C 15,1.4 14.6,1 14,1 H 12 C 11.4,1 11,1.4 11,2 V 8.6 C 11,9 10.5,9.3 10.1,9 L 9.1,8 C 8.9,7.8 8.7,7.7 8.4,7.7 8.1,7.7 7.9,7.8 7.7,8 L 6.3,9.4 C 6.1,9.6 6,9.8 6,10.1 c 0,0.3 0.1,0.5 0.3,0.7 z"
id="path4"
inkscape:connector-curvature="0" />
inkscape:connector-curvature="0"
style="fill:#ff0000" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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">
<defs
id="defs3865" />
<sodipodi:namedview
@@ -26,16 +26,16 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="354.2246"
inkscape:cy="117.98098"
inkscape:cx="8.4698372"
inkscape:cy="118.04149"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="false"
inkscape:window-width="1360"
inkscape:window-height="707"
inkscape:window-width="1366"
inkscape:window-height="703"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
fit-margin-bottom="-0.2"
fit-margin-top="0"
@@ -58,9 +58,9 @@
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-12.111406,-169.00616)">
transform="translate(-12.122116,-167.33593)">
<g
transform="matrix(0.08047549,0,0,0.0997485,-51.028672,86.747244)"
transform="matrix(0.08875425,0,0,0.11490723,-57.513376,72.576168)"
id="g10674"
style="stroke-width:0.77710575">
<path

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

220
fpakman/resources/img/history.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

155
fpakman/resources/img/info.svg Executable file
View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="24"
height="24"
viewBox="0 0 24 24"
xml:space="preserve"
sodipodi:docname="info.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata128"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs126"><linearGradient
inkscape:collect="always"
id="linearGradient153"><stop
style="stop-color:#0000ff;stop-opacity:1;"
offset="0"
id="stop149" /><stop
style="stop-color:#0000ff;stop-opacity:0;"
offset="1"
id="stop151" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient153"
id="linearGradient155"
x1="-180.77992"
y1="-30.518711"
x2="337.13013"
y2="868.49902"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient153"
id="linearGradient157"
gradientUnits="userSpaceOnUse"
x1="-180.77992"
y1="-30.518711"
x2="337.13013"
y2="868.49902" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview124"
showgrid="false"
showborder="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.8698707"
inkscape:cx="143.59281"
inkscape:cy="26.740827"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g93"
transform="translate(0,-414.534)">
</g>
<g
id="g95"
transform="translate(0,-414.534)">
</g>
<g
id="g97"
transform="translate(0,-414.534)">
</g>
<g
id="g99"
transform="translate(0,-414.534)">
</g>
<g
id="g101"
transform="translate(0,-414.534)">
</g>
<g
id="g103"
transform="translate(0,-414.534)">
</g>
<g
id="g105"
transform="translate(0,-414.534)">
</g>
<g
id="g107"
transform="translate(0,-414.534)">
</g>
<g
id="g109"
transform="translate(0,-414.534)">
</g>
<g
id="g111"
transform="translate(0,-414.534)">
</g>
<g
id="g113"
transform="translate(0,-414.534)">
</g>
<g
id="g115"
transform="translate(0,-414.534)">
</g>
<g
id="g117"
transform="translate(0,-414.534)">
</g>
<g
id="g119"
transform="translate(0,-414.534)">
</g>
<g
id="g121"
transform="translate(0,-414.534)">
</g>
<g
id="g96"
transform="scale(0.50000002)"><g
style="fill:url(#linearGradient155);fill-opacity:1"
transform="scale(0.10945609,0.1094556)"
id="g91">
<path
style="fill:url(#linearGradient157);fill-opacity:1"
inkscape:connector-curvature="0"
id="path89"
d="M 409.133,109.203 C 389.525,75.611 362.928,49.014 329.335,29.407 295.736,9.801 259.058,0 219.273,0 179.492,0 142.803,9.801 109.21,29.407 75.615,49.011 49.018,75.608 29.41,109.203 9.801,142.8 0,179.489 0,219.267 c 0,39.78 9.804,76.463 29.407,110.062 19.607,33.592 46.204,60.189 79.799,79.798 33.597,19.605 70.283,29.407 110.063,29.407 39.78,0 76.47,-9.802 110.065,-29.407 33.593,-19.602 60.189,-46.206 79.795,-79.798 19.603,-33.596 29.403,-70.284 29.403,-110.062 0.001,-39.782 -9.8,-76.472 -29.399,-110.064 z M 182.727,54.813 c 0,-2.666 0.855,-4.853 2.57,-6.565 1.712,-1.711 3.903,-2.57 6.567,-2.57 h 54.82 c 2.662,0 4.853,0.859 6.561,2.57 1.711,1.712 2.573,3.899 2.573,6.565 v 45.682 c 0,2.664 -0.862,4.854 -2.573,6.564 -1.708,1.712 -3.898,2.568 -6.561,2.568 h -54.82 c -2.664,0 -4.854,-0.856 -6.567,-2.568 -1.715,-1.709 -2.57,-3.9 -2.57,-6.564 z m 109.632,301.496 c 0,2.662 -0.863,4.853 -2.57,6.561 -1.704,1.714 -3.895,2.57 -6.563,2.57 H 155.317 c -2.667,0 -4.854,-0.856 -6.567,-2.57 -1.712,-1.708 -2.568,-3.898 -2.568,-6.564 v -45.682 c 0,-2.67 0.856,-4.853 2.568,-6.567 1.713,-1.708 3.903,-2.57 6.567,-2.57 h 27.41 v -91.358 h -27.41 c -2.667,0 -4.853,-0.855 -6.567,-2.568 -1.712,-1.711 -2.568,-3.901 -2.568,-6.567 v -45.679 c 0,-2.666 0.856,-4.853 2.568,-6.567 1.715,-1.713 3.905,-2.568 6.567,-2.568 h 91.367 c 2.662,0 4.853,0.855 6.561,2.568 1.711,1.714 2.573,3.901 2.573,6.567 v 146.179 h 27.401 c 2.669,0 4.859,0.855 6.57,2.566 1.704,1.712 2.566,3.901 2.566,6.567 v 45.683 h 0.004 z" />
</g><path
inkscape:connector-curvature="0"
id="path88"
d="m 16.36476,36.447391 v -3.208779 h 1.871787 1.871787 V 27.890649 22.542686 H 18.236547 16.36476 V 19.333908 16.12513 h 5.615361 5.615362 v 8.556741 8.556741 h 2.139185 2.139186 v 3.208779 3.208778 H 24.119307 16.36476 Z"
style="fill:#ffffff;stroke-width:0.53479636" /><path
inkscape:connector-curvature="0"
id="path90"
d="M 20.571824,11.490228 C 20.375732,11.294136 20.215294,9.8501861 20.215294,8.2814501 V 5.429203 h 3.743574 3.743574 v 3.208778 3.208778 h -3.387043 c -1.862874,0 -3.547483,-0.160439 -3.743575,-0.356531 z"
style="fill:#ffffff;stroke-width:0.53479636" /></g></svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

120
fpakman/resources/img/install.svg Executable file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns: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="12"
height="12"
viewBox="0 0 12 12"
xml:space="preserve"
sodipodi:docname="install.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata135"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs133" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview131"
showgrid="false"
showborder="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.9124627"
inkscape:cx="58.262905"
inkscape:cy="42.640588"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<g
id="g98"
transform="scale(0.034381)"
style="fill:#00ff00">
<path
d="m 349.03,141.226 v 66.579 c 0,5.012 -4.061,9.079 -9.079,9.079 H 216.884 v 123.067 c 0,5.019 -4.067,9.079 -9.079,9.079 h -66.579 c -5.009,0 -9.079,-4.061 -9.079,-9.079 V 216.884 H 9.079 C 4.063,216.884 0,212.817 0,207.805 v -66.579 c 0,-5.013 4.063,-9.079 9.079,-9.079 H 132.147 V 9.079 C 132.147,4.061 136.216,0 141.226,0 h 66.579 c 5.012,0 9.079,4.061 9.079,9.079 v 123.068 h 123.067 c 5.019,0 9.079,4.066 9.079,9.079 z"
id="path96"
inkscape:connector-curvature="0"
style="fill:#00ff00" />
</g>
<g
id="g100"
transform="translate(0,-337.03)">
</g>
<g
id="g102"
transform="translate(0,-337.03)">
</g>
<g
id="g104"
transform="translate(0,-337.03)">
</g>
<g
id="g106"
transform="translate(0,-337.03)">
</g>
<g
id="g108"
transform="translate(0,-337.03)">
</g>
<g
id="g110"
transform="translate(0,-337.03)">
</g>
<g
id="g112"
transform="translate(0,-337.03)">
</g>
<g
id="g114"
transform="translate(0,-337.03)">
</g>
<g
id="g116"
transform="translate(0,-337.03)">
</g>
<g
id="g118"
transform="translate(0,-337.03)">
</g>
<g
id="g120"
transform="translate(0,-337.03)">
</g>
<g
id="g122"
transform="translate(0,-337.03)">
</g>
<g
id="g124"
transform="translate(0,-337.03)">
</g>
<g
id="g126"
transform="translate(0,-337.03)">
</g>
<g
id="g128"
transform="translate(0,-337.03)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

477
fpakman/resources/img/logo.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 12 12"
xml:space="preserve"
sodipodi:docname="cross_red.svg"
width="12"
height="12"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata1018"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs1016" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview1014"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showborder="false"
inkscape:zoom="1.7430481"
inkscape:cx="39.29923"
inkscape:cy="73.435604"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
id="XMLID_90_"
d="M 0.18530763,10.025545 1.9744745,11.814702 C 2.093058,11.933327 2.2540227,12 2.4217347,12 2.5894888,12 2.7504115,11.93333 2.8689948,11.814702 L 5.9999843,8.6837266 9.1309735,11.814702 C 9.2495992,11.933327 9.4105217,12 9.5782337,12 c 0.167754,0 0.3286768,-0.06667 0.4473023,-0.185298 l 1.789167,-1.789157 c 0.247034,-0.2470354 0.247034,-0.6475284 0,-0.8945604 L 8.6837558,6.0000066 11.814703,2.8689866 C 11.933371,2.7503616 12,2.5894836 12,2.4217286 c 0,-0.167795 -0.06663,-0.328675 -0.185297,-0.4473 L 10.025578,0.18526863 c -0.2469922,-0.247032 -0.6475697,-0.247032 -0.8945622,4.3e-5 L 6.0000263,3.3162466 2.8689948,0.18531163 c -0.2469925,-0.247035 -0.6475699,-0.247035 -0.8945624,0 L 0.18530763,1.9744286 C 0.0666399,2.0930536 1.054262e-5,2.2539336 1.054262e-5,2.4216866 c 0,0.167755 0.06662935738,0.328635 0.18529708738,0.44726 l 3.13094717,3.131018 -3.13094717,3.13102 c -0.24707684,0.247032 -0.24707684,0.647525 0,0.8945604 z"
inkscape:connector-curvature="0"
style="fill:#ff0000;stroke-width:0.0421704" />
<g
id="g983"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g985"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g987"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g989"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g991"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g993"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g995"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g997"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g999"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1001"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1003"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1005"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1007"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1009"
transform="translate(2.5e-4,-272.56001)">
</g>
<g
id="g1011"
transform="translate(2.5e-4,-272.56001)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

81
fpakman/resources/img/refresh.svg Normal file → Executable file
View File

@@ -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"><metadata
sodipodi:docname="refresh_2.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata45"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs43" /><sodipodi:namedview
id="defs43"><linearGradient
inkscape:collect="always"
id="linearGradient540"><stop
style="stop-color:#0088aa;stop-opacity:1;"
offset="0"
id="stop536" /><stop
style="stop-color:#0088aa;stop-opacity:0;"
offset="1"
id="stop538" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient540"
id="linearGradient542"
x1="-130.74721"
y1="-214.80638"
x2="-556.49438"
y2="110.01643"
gradientUnits="userSpaceOnUse" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
@@ -31,16 +48,16 @@
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1360"
inkscape:window-height="707"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview41"
showgrid="false"
showborder="false"
inkscape:zoom="0.48437082"
inkscape:cx="306.00515"
inkscape:cy="-4.129072"
inkscape:zoom="1.9374833"
inkscape:cx="165.58246"
inkscape:cy="-10.288149"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
@@ -49,81 +66,81 @@
fit-margin-bottom="0" />
<g
id="g8"
transform="matrix(0.09887825,0,0,0.09235884,-1.5881978,0)"
style="fill:#0088aa">
transform="matrix(0.10547014,0,0,0.0985161,-1.6940777,0)"
style="fill:url(#linearGradient542);fill-opacity:1">
<g
id="g6"
style="fill:#0088aa">
style="fill:url(#linearGradient542);fill-opacity:1">
<path
d="m 55.323,203.641 c 15.664,0 29.813,-9.405 35.872,-23.854 25.017,-59.604 83.842,-101.61 152.42,-101.61 37.797,0 72.449,12.955 100.23,34.442 l -21.775,3.371 c -7.438,1.153 -13.224,7.054 -14.232,14.512 -1.01,7.454 3.008,14.686 9.867,17.768 l 119.746,53.872 c 5.249,2.357 11.33,1.904 16.168,-1.205 4.83,-3.114 7.764,-8.458 7.796,-14.208 l 0.621,-131.943 c 0.042,-7.506 -4.851,-14.144 -12.024,-16.332 -7.185,-2.188 -14.947,0.589 -19.104,6.837 L 414.403,70.096 C 370.398,26.778 310.1,0 243.615,0 142.806,0 56.133,61.562 19.167,149.06 c -5.134,12.128 -3.84,26.015 3.429,36.987 7.269,10.976 19.556,17.594 32.727,17.594 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:#0088aa" />
style="fill:url(#linearGradient542);fill-opacity:1" />
<path
d="m 464.635,301.184 c -7.27,-10.977 -19.558,-17.594 -32.728,-17.594 -15.664,0 -29.813,9.405 -35.872,23.854 -25.018,59.604 -83.843,101.61 -152.42,101.61 -37.798,0 -72.45,-12.955 -100.232,-34.442 l 21.776,-3.369 c 7.437,-1.153 13.223,-7.055 14.233,-14.514 1.009,-7.453 -3.008,-14.686 -9.867,-17.768 L 49.779,285.089 c -5.25,-2.356 -11.33,-1.905 -16.169,1.205 -4.829,3.114 -7.764,8.458 -7.795,14.207 l -0.622,131.943 c -0.042,7.506 4.85,14.144 12.024,16.332 7.185,2.188 14.948,-0.59 19.104,-6.839 l 16.505,-24.805 c 44.004,43.32 104.303,70.098 170.788,70.098 100.811,0 187.481,-61.561 224.446,-149.059 5.137,-12.128 3.843,-26.014 -3.425,-36.987 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#0088aa" />
style="fill:url(#linearGradient542);fill-opacity:1" />
</g>
</g>
<g
id="g10"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g12"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g14"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g16"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g18"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g20"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g22"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g24"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g26"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g28"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g30"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g32"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g34"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g36"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
<g
id="g38"
transform="translate(-16.062155,-442.23)">
transform="translate(-16.062155,-439.23)">
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

120
fpakman/resources/img/search.svg Executable file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 12 12"
xml:space="preserve"
sodipodi:docname="search.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
width="12"
height="12"><metadata
id="metadata126"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs124" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="739"
id="namedview122"
showgrid="false"
showborder="false"
inkscape:zoom="0.53133476"
inkscape:cx="834.65593"
inkscape:cy="-101.88082"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="Search"
transform="scale(0.04794017)"
style="fill:#999999">
<path
style="clip-rule:evenodd;fill:#999999;fill-rule:evenodd"
d="m 244.186,214.604 -54.379,-54.378 c -0.289,-0.289 -0.628,-0.491 -0.93,-0.76 10.7,-16.231 16.945,-35.66 16.945,-56.554 C 205.822,46.075 159.747,0 102.911,0 46.075,0 0,46.075 0,102.911 c 0,56.835 46.074,102.911 102.91,102.911 20.895,0 40.323,-6.245 56.554,-16.945 0.269,0.301 0.47,0.64 0.759,0.929 l 54.38,54.38 c 8.169,8.168 21.413,8.168 29.583,0 8.168,-8.169 8.168,-21.413 0,-29.582 z M 102.911,170.146 c -37.134,0 -67.236,-30.102 -67.236,-67.235 0,-37.134 30.103,-67.236 67.236,-67.236 37.132,0 67.235,30.103 67.235,67.236 0,37.133 -30.103,67.235 -67.235,67.235 z"
id="path88"
inkscape:connector-curvature="0" />
</g>
<g
id="g91"
transform="translate(0,-238.312)">
</g>
<g
id="g93"
transform="translate(0,-238.312)">
</g>
<g
id="g95"
transform="translate(0,-238.312)">
</g>
<g
id="g97"
transform="translate(0,-238.312)">
</g>
<g
id="g99"
transform="translate(0,-238.312)">
</g>
<g
id="g101"
transform="translate(0,-238.312)">
</g>
<g
id="g103"
transform="translate(0,-238.312)">
</g>
<g
id="g105"
transform="translate(0,-238.312)">
</g>
<g
id="g107"
transform="translate(0,-238.312)">
</g>
<g
id="g109"
transform="translate(0,-238.312)">
</g>
<g
id="g111"
transform="translate(0,-238.312)">
</g>
<g
id="g113"
transform="translate(0,-238.312)">
</g>
<g
id="g115"
transform="translate(0,-238.312)">
</g>
<g
id="g117"
transform="translate(0,-238.312)">
</g>
<g
id="g119"
transform="translate(0,-238.312)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -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">
<defs
id="defs2" />
<sodipodi:namedview
@@ -26,8 +26,8 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="911.42852"
inkscape:cy="-80.000008"
inkscape:cx="398.57142"
inkscape:cy="-80.000033"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
@@ -38,7 +38,7 @@
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1366"
inkscape:window-height="739"
inkscape:window-height="703"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1" />
@@ -50,7 +50,7 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
@@ -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)">
<circle
style="fill:#b3b3b3;stroke-width:0.04630494"
style="fill:#b3b3b3;stroke-width:0.02469599"
id="path10"
cx="-192.10638"
cy="121.71353"
r="5.953125" />
cx="-194.88452"
cy="124.49165"
r="3.175" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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">
<defs
id="defs2" />
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient66">
<stop
style="stop-color:#55d400;stop-opacity:1;"
offset="0"
id="stop62" />
<stop
style="stop-color:#55d400;stop-opacity:0;"
offset="1"
id="stop64" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient66"
id="linearGradient68"
x1="-210.35391"
y1="112.4323"
x2="-215.96657"
y2="126.86485"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.53333336,0,0,0.53333336,-92.427766,59.577773)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
@@ -25,9 +49,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="802.85709"
inkscape:cy="-80.000008"
inkscape:zoom="0.98994949"
inkscape:cx="185.81986"
inkscape:cy="20.785442"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
@@ -38,7 +62,7 @@
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1366"
inkscape:window-height="739"
inkscape:window-height="703"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1" />
@@ -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)">
<circle
style="fill:#66ff00;stroke-width:0.04630494"
style="fill:url(#linearGradient68);fill-opacity:1;stroke-width:0.02469597"
id="path10"
cx="-192.10638"
cy="121.71353"
r="5.953125" />
cx="-194.88451"
cy="124.49166"
r="3.175" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24"
viewBox="-40 0 18 18.000056"
width="24"
version="1.1"
id="svg72"
sodipodi:docname="uninstall.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata78">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs76" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="703"
id="namedview74"
showgrid="false"
showborder="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="5.7611241"
inkscape:cx="40.648821"
inkscape:cy="17.781204"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
inkscape:current-layer="svg72"
units="px" />
<g
id="g5907"
transform="matrix(0.50000001,0,0,0.49999997,-20.000045,0)">
<g
transform="matrix(0.10380557,0,0,0.08430914,-35.847729,0)"
id="g84">
<path
d="m 192.40098,154.70443 c -5.52344,0 -10,4.47657 -10,10 v 189 c 0,5.51954 4.47656,10 10,10 5.52343,0 10,-4.48046 10,-10 v -189 c 0,-5.52343 -4.47657,-10 -10,-10 z m 0,0"
id="path64"
inkscape:connector-curvature="0" />
<path
d="m 74.400978,154.70443 c -5.52344,0 -10,4.47657 -10,10 v 189 c 0,5.51954 4.47656,10 10,10 5.52344,0 10,-4.48046 10,-10 v -189 c 0,-5.52343 -4.47656,-10 -10,-10 z m 0,0"
id="path66"
inkscape:connector-curvature="0" />
<path
d="m -11.599024,127.1224 v 246.37891 c 0,14.5625 5.3398433,28.23828 14.6679683,38.05078 9.2851557,9.83984 22.2070317,15.42578 35.7304687,15.44922 H 228.00254 c 13.52734,-0.0234 26.44922,-5.60938 35.73047,-15.44922 9.32812,-9.8125 14.66797,-23.48828 14.66797,-38.05078 V 127.1224 C 296.94395,122.20053 308.95957,104.28647 306.4791,85.259126 303.99473,66.235689 287.7877,52.00522 268.6002,52.001314 h -51.19922 v -12.5 c 0.0586,-10.511719 -4.09766,-20.605469 -11.53907,-28.03125 -7.4414,-7.4218751 -17.55078,-11.55468758 -28.0625,-11.46875008 H 89.002538 C 78.490818,-0.08462358 68.381448,4.0481889 60.940038,11.470064 53.498632,18.895845 49.342382,28.989595 49.400976,39.501314 v 12.5 H -1.7982427 c -19.1875003,0.0039 -35.3945313,14.234375 -37.8789073,33.257812 -2.480468,19.027344 9.535157,36.941404 28.078126,41.863274 z M 228.00254,407.00131 H 38.799413 c -17.097656,0 -30.3984367,-14.6875 -30.3984367,-33.5 v -245.5 H 258.40098 v 245.5 c 0,18.8125 -13.30078,33.5 -30.39844,33.5 z M 69.400978,39.501314 c -0.0664,-5.207031 1.98047,-10.21875 5.67578,-13.894531 3.69141,-3.675781 8.71484,-5.695313 13.92578,-5.605469 h 88.796872 c 5.21094,-0.08984 10.23438,1.929688 13.92579,5.605469 3.69531,3.671875 5.74218,8.6875 5.67578,13.894531 v 12.5 H 69.400978 Z m -71.1992207,32.5 H 268.6002 c 9.9414,0 18,8.058594 18,18 0,9.941406 -8.0586,17.999996 -18,17.999996 H -1.7982427 c -9.9414073,0 -18.0000003,-8.05859 -18.0000003,-17.999996 0,-9.941406 8.058593,-18 18.0000003,-18 z m 0,0"
id="path68"
inkscape:connector-curvature="0" />
<path
d="m 133.40098,154.70443 c -5.52344,0 -10,4.47657 -10,10 v 189 c 0,5.51954 4.47656,10 10,10 5.52343,0 10,-4.48046 10,-10 v -189 c 0,-5.52343 -4.47657,-10 -10,-10 z m 0,0"
id="path70"
inkscape:connector-curvature="0" />
</g>
<path
inkscape:connector-curvature="0"
id="path3791"
d="m -32.347465,34.215765 c -1.136967,-0.263564 -1.85228,-0.770702 -2.285878,-1.620623 -0.244025,-0.478328 -0.252697,-0.85963 -0.252697,-11.110455 V 10.869559 h 12.888149 12.8881497 l -0.0028,10.577396 c -0.0027,10.017853 -0.01562,10.604942 -0.243951,11.098129 -0.295923,0.639162 -0.8228787,1.147628 -1.5106647,1.457656 -0.486498,0.219295 -1.201157,0.235997 -10.870308,0.254055 -5.692267,0.01062 -10.46674,-0.0078 -10.609942,-0.04103 z m 4.983091,-3.760996 0.289334,-0.202658 v -8.369266 c 0,-8.31107 -0.0019,-8.370969 -0.270765,-8.614305 -0.358608,-0.324535 -1.182796,-0.324535 -1.541404,0 -0.268882,0.243336 -0.270765,0.303235 -0.270765,8.614305 v 8.369266 l 0.289334,0.202658 c 0.159133,0.11146 0.497593,0.202656 0.752133,0.202656 0.25454,0 0.593,-0.0912 0.752133,-0.202656 z m 6.118617,0 0.289333,-0.202658 v -8.369266 c 0,-8.31107 -0.0019,-8.370969 -0.270764,-8.614305 -0.358608,-0.324535 -1.182797,-0.324535 -1.541404,0 -0.268883,0.243336 -0.270765,0.303235 -0.270765,8.614305 v 8.369266 l 0.289333,0.202658 c 0.159134,0.11146 0.497593,0.202656 0.752133,0.202656 0.25454,0 0.593001,-0.0912 0.752134,-0.202656 z m 6.118616,0 0.289334,-0.202658 v -8.369266 c 0,-8.31107 -0.0019,-8.370969 -0.270765,-8.614305 -0.358608,-0.324535 -1.182796,-0.324535 -1.541404,0 -0.268882,0.243336 -0.270765,0.303235 -0.270765,8.614305 v 8.369266 l 0.289334,0.202658 c 0.159132,0.11146 0.497593,0.202656 0.752133,0.202656 0.254539,0 0.593,-0.0912 0.752133,-0.202656 z"
style="fill:#b3b3b3;stroke-width:0.13018332" />
<path
inkscape:connector-curvature="0"
id="path3793"
d="m -36.877845,8.8644087 c -0.540224,-0.2384603 -0.844092,-0.6148864 -0.931583,-1.1540278 -0.06403,-0.3945707 -0.01083,-0.5268359 0.386094,-0.9599757 l 0.460354,-0.5023539 14.753284,-0.034506 c 10.894353,-0.025481 14.8480787,0.00467 15.1156987,0.115259 0.791461,0.3270622 1.121594,1.3085049 0.671312,1.9957216 -0.490508,0.7486088 0.06973,0.7229272 -15.6494407,0.7173712 -11.671083,-0.00413 -14.489515,-0.037913 -14.805719,-0.1774884 z"
style="fill:#b3b3b3;stroke-width:0.13018332" />
<path
inkscape:connector-curvature="0"
id="path3795"
d="m -28.586127,3.581076 c 0.04302,-0.5947544 0.131512,-0.8607897 0.373651,-1.1233362 0.63458,-0.6880588 0.750066,-0.7010134 6.249312,-0.7010134 4.435032,0 5.151954,0.027404 5.540514,0.2117885 0.718185,0.3408012 0.959962,0.7509159 0.959962,1.6283345 V 4.360393 h -6.589905 -6.589904 z"
style="fill:#b3b3b3;stroke-width:0.13018332" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -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">
<metadata
id="metadata14">
<rdf:RDF>
@@ -28,7 +29,47 @@
</rdf:RDF>
</metadata>
<defs
id="defs12" />
id="defs12">
<linearGradient
inkscape:collect="always"
id="linearGradient309">
<stop
style="stop-color:#55d400;stop-opacity:1;"
offset="0"
id="stop305" />
<stop
style="stop-color:#55d400;stop-opacity:0;"
offset="1"
id="stop307" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient309"
id="linearGradient311"
x1="-12.490223"
y1="5.5246015"
x2="-20.59199"
y2="18.300463"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient309"
id="linearGradient313"
gradientUnits="userSpaceOnUse"
x1="-12.490223"
y1="5.5246015"
x2="-20.59199"
y2="18.300463" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient309"
id="linearGradient315"
gradientUnits="userSpaceOnUse"
x1="-12.490223"
y1="5.5246015"
x2="-20.59199"
y2="18.300463" />
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
@@ -39,13 +80,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="-4.5169491"
inkscape:cy="20.813559"
inkscape:zoom="3.2091769"
inkscape:cx="11.132699"
inkscape:cy="36.020085"
inkscape:window-x="0"
inkscape:window-y="260"
inkscape:window-maximized="1"
@@ -56,17 +97,21 @@
fit-margin-bottom="0" />
<g
id="g6"
transform="matrix(1.7307692,0,0,1.875,0,-1.875)"
style="fill:#00ff00">
transform="matrix(1.8461538,0,0,2,0,-2)"
style="fill:url(#linearGradient311);fill-opacity:1">
<path
d="m 25,17 h -2 c -0.6,0 -1,0.4 -1,1 v 2.5 C 22,20.8 21.8,21 21.5,21 H 4.5 C 4.2,21 4,20.8 4,20.5 V 18 C 4,17.4 3.6,17 3,17 H 1 c -0.6,0 -1,0.4 -1,1 v 6 c 0,0.6 0.4,1 1,1 h 24 c 0.6,0 1,-0.4 1,-1 v -6 c 0,-0.6 -0.4,-1 -1,-1 z"
id="path2"
inkscape:connector-curvature="0"
style="fill:#00ff00" />
style="fill:url(#linearGradient313);fill-opacity:1" />
<path
d="m 12.3,16.7 c 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 l 6,-6 C 19.9,10.5 20,10.3 20,10 20,9.7 19.9,9.5 19.7,9.3 L 18.3,7.9 C 18.1,7.7 17.9,7.6 17.6,7.6 c -0.3,0 -0.5,0.1 -0.7,0.3 l -1,1 C 15.6,9.2 15,9 15,8.5 V 2 C 15,1.4 14.6,1 14,1 H 12 C 11.4,1 11,1.4 11,2 V 8.6 C 11,9 10.5,9.3 10.1,9 L 9.1,8 C 8.9,7.8 8.7,7.7 8.4,7.7 8.1,7.7 7.9,7.8 7.7,8 L 6.3,9.4 C 6.1,9.6 6,9.8 6,10.1 c 0,0.3 0.1,0.5 0.3,0.7 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#00ff00" />
style="fill:url(#linearGradient315);fill-opacity:1" />
</g>
<g
transform="matrix(3.7795276,0,0,3.7795276,668.39708,-480.81644)"
id="layer1"
inkscape:label="Layer 1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

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

View File

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

71
fpakman/view/qt/about.py Normal file
View File

@@ -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'] + ": <a href='{url}'>{url}</a>".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("<a href='{}'>{}</a>".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()

View File

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

32
fpakman/view/qt/dialog.py Normal file
View File

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

View File

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

39
fpakman/view/qt/info.py Normal file
View File

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

45
fpakman/view/qt/root.py Normal file
View File

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

View File

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

186
fpakman/view/qt/thread.py Normal file
View File

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

View File

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

View File

@@ -1,2 +1,3 @@
pyqt5>=5.12
requests>=2.22
requests>=2.22
colorama>=0.4.1