This commit is contained in:
Vinícius Moreira
2020-01-14 11:47:12 -03:00
committed by GitHub
35 changed files with 529 additions and 191 deletions

View File

@@ -4,22 +4,43 @@ 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.8.1]
## [0.8.1] 2020-01-14
### Features:
- Flatpak:
- allow the user to choose the application installation level: **user** or **system** [#47](https://github.com/vinifmor/bauh/issues/47)
- able to deal with user and system applications / runtimes [#47](https://github.com/vinifmor/bauh/issues/47)
- able to list partial updates for Flatpak >= 1.4
- new configuration file located at **~/.config/bauh/flatpak.yml** ( it allows to define a default installation level )
### Improvements
- All icons are now SVG files
- HDPI support ( by [octopusSD](https://github.com/octopusSD) )
- HDPI support improvements ( by [octopusSD](https://github.com/octopusSD) )
- Flatpak:
- the application name tooltip now displays the installation level. e.g: **gedit ( system )**
- info window displaying the installation level
- "remote not set" warning dropped in favor of the new behavior: automatically adds Flathub as the default remote at the user level
- Snap:
- snapd checking routine refactored
- Web:
- not using HTTP sessions anymore to perform the searches. It seems to avoid URLs not being found after an internet drop event.
- not using HTTP sessions anymore to perform the searches. It seems to avoid URLs not being found after an internet drop event
- supporting JPEG images as custom icons
- UI:
- widgets visibility settings: the main widgets now should always be visible ( e.g: toolbar buttons )
- scaling
### Fixes
- missing categories i18n [48](https://github.com/vinifmor/bauh/issues/48)
- not verifying if an icon path is a file
- missing categories i18n [#48](https://github.com/vinifmor/bauh/issues/48)
- Flatpak:
- updating application dependencies during updating and downgrading
- Web:
- not handling HTTP connection issues
- not passing the Home path as a String ( does not work in Python 3.5 )
- not passing the Home path as a String for subprocesses ( an exception happens for Python 3.5 )
- UI:
- not verifying if an icon path is a file
- minor fixes
### UI
- Default **Type** icon removed from the Type filter to make the design more consistent with the Category filter.
- Default **Type** icon removed from the Type filter to make the design more consistent
## [0.8.0] 2019-12-24
### Features

View File

@@ -21,6 +21,7 @@ To contribute with this project, have a look at [CONTRIBUTING.md](https://github
- **pip3**
- **python3-requests**
- **python-yaml**
- **qt5dxcb-plugin**
- **python3-venv** ( only for [Manual installation](https://github.com/vinifmor/bauh/tree/wgem#manual-installation) )
- **libappindicator3** ( for the **tray mode** in GTK3 desktop environments )
@@ -99,6 +100,11 @@ Before uninstalling bauh via your package manager, consider executing `bauh --re
![flatpak_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/flatpak/search.gif)
- The configuration file is located at **~/.config/bauh/flatpak.yml** and it allows the following customizations:
```
installation_level: null # defines a default installation level: user or system. ( the popup will not be displayed if a value is defined )
```
- Required dependencies:
- Any distro: **flatpak**

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import List, Tuple
from bauh.api.abstract.view import MessageType, ViewComponent
@@ -65,3 +65,9 @@ class ProcessWatcher:
"""
:return: if the use requested to stop the process.
"""
def request_root_password(self) -> Tuple[str, bool]:
"""
asks the root password for the user
:return: a tuple with the typed password and if it is valid
"""

View File

@@ -175,6 +175,12 @@ class SoftwarePackage(ABC):
"""
return not self.installed
def get_name_tooltip(self) -> str:
"""
:return: the application name that should be displayed on the UI tooltips.
"""
return self.name
def __str__(self):
return '{} (id={}, name={})'.format(self.__class__.__name__, self.id, self.name)

View File

@@ -4,7 +4,6 @@ from threading import Thread
import urllib3
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication
from bauh import __version__, __app_name__, app_args, ROOT_DIR
@@ -15,7 +14,7 @@ from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.core.downloader import AdaptableFileDownloader
from bauh.view.qt.systray import TrayIcon
from bauh.view.qt.window import ManageWindow
from bauh.view.util import util, logs, resource, translation
from bauh.view.util import util, logs, translation
from bauh.view.util.cache import DefaultMemoryCacheFactory, CacheCleaner
from bauh.view.util.disk import DefaultDiskCacheLoaderFactory
from bauh.view.util.translation import I18n

5
bauh/commons/user.py Normal file
View File

@@ -0,0 +1,5 @@
import os
def is_root():
return os.getuid() == 0

View File

@@ -27,11 +27,10 @@ from bauh.gems.appimage.config import read_config
from bauh.gems.appimage.model import AppImage
from bauh.gems.appimage.worker import DatabaseUpdater
HOME_PATH = str(Path.home())
DB_APPS_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/apps.db')
DB_RELEASES_PATH = '{}/{}'.format(HOME_PATH, '.local/share/bauh/appimage/releases.db')
DB_APPS_PATH = '{}/{}'.format(str(Path.home()), '.local/share/bauh/appimage/apps.db')
DB_RELEASES_PATH = '{}/{}'.format(str(Path.home()), '.local/share/bauh/appimage/releases.db')
DESKTOP_ENTRIES_PATH = '{}/.local/share/applications'.format(HOME_PATH)
DESKTOP_ENTRIES_PATH = '{}/.local/share/applications'.format(str(Path.home()))
RE_DESKTOP_EXEC = re.compile(r'Exec\s*=\s*.+\n')
RE_DESKTOP_ICON = re.compile(r'Icon\s*=\s*.+\n')
@@ -298,7 +297,7 @@ class AppImageManager(SoftwareManager):
file_path = out_dir + '/' + file_name
downloaded = self.file_downloader.download(file_url=pkg.url_download, watcher=watcher,
output_path=file_path, cwd=HOME_PATH)
output_path=file_path, cwd=str(Path.home()))
if downloaded:
watcher.change_substatus(self.i18n['appimage.install.permission'].format(bold(file_name)))

View File

@@ -1,3 +1,6 @@
import os
from pathlib import Path
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/flatpak/suggestions.txt'
CONFIG_FILE = '{}/.config/bauh/flatpak.yml'.format(Path.home())

View File

@@ -0,0 +1,7 @@
from bauh.commons.config import read_config as read
from bauh.gems.flatpak import CONFIG_FILE
def read_config(update_file: bool = False) -> dict:
template = {'installation_level': None}
return read(CONFIG_FILE, template, update_file=update_file)

View File

@@ -9,9 +9,11 @@ from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion, \
SuggestionPriority
from bauh.api.abstract.view import MessageType
from bauh.commons import user
from bauh.commons.html import strip_html, bold
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE
from bauh.commons.system import SystemProcess, ProcessHandler, SimpleProcess
from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE
from bauh.gems.flatpak.config import read_config
from bauh.gems.flatpak.constants import FLATHUB_API_URL
from bauh.gems.flatpak.model import FlatpakApplication
from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
@@ -37,7 +39,7 @@ class FlatpakManager(SoftwareManager):
def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> FlatpakApplication:
app = FlatpakApplication(**app_json)
app = FlatpakApplication(**app_json, i18n=self.i18n)
app.installed = installed
api_data = self.api_cache.get(app_json['id'])
@@ -57,12 +59,27 @@ class FlatpakManager(SoftwareManager):
return app
def _get_search_remote(self) -> str:
remotes = flatpak.list_remotes()
if remotes['system']:
remote_level = 'system'
elif remotes['user']:
remote_level = 'user'
else:
remote_level = 'user'
ProcessHandler().handle_simple(flatpak.set_default_remotes(remote_level))
return remote_level
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult:
if is_url:
return SearchResult([], [], 0)
remote_level = self._get_search_remote()
res = SearchResult([], [], 0)
apps_found = flatpak.search(flatpak.get_version(), words)
apps_found = flatpak.search(flatpak.get_version(), words, remote_level)
if apps_found:
already_read = set()
@@ -101,27 +118,44 @@ class FlatpakManager(SoftwareManager):
models = []
if installed:
update_map = None
if thread_updates:
thread_updates.join()
update_map = updates[0]
for app_json in installed:
model = self._map_to_model(app_json=app_json, installed=True,
disk_loader=disk_loader, internet=internet_available)
if version >= '1.5.0':
model.update = '{}/{}'.format(app_json['id'], app_json['branch']) in updates[0] if updates else None
else:
model.update = app_json['ref'] in updates[0] if updates else None
model.update = None
models.append(model)
if update_map and (update_map['full'] or update_map['partial']):
if version >= '1.4.0':
update_id = '{}/{}/{}'.format(app_json['id'], app_json['branch'], app_json['installation'])
if update_map['full'] and update_id in update_map['full']:
model.update = True
if update_map['partial']:
for partial in update_map['partial']:
partial_data = partial.split('/')
if app_json['id'] in partial_data[0] and\
app_json['branch'] == partial_data[1] and\
app_json['installation'] == partial_data[2]:
partial_model = model.gen_partial(partial.split('/')[0])
partial_model.update = True
models.append(partial_model)
else:
model.update = '{}/{}'.format(app_json['installation'], app_json['ref']) in update_map['full']
return SearchResult(models, None, len(models))
def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
pkg.commit = flatpak.get_commit(pkg.id, pkg.branch)
pkg.commit = flatpak.get_commit(pkg.id, pkg.branch, pkg.installation)
watcher.change_progress(10)
watcher.change_substatus(self.i18n['flatpak.downgrade.commits'])
commits = flatpak.get_app_commits(pkg.ref, pkg.origin)
commits = flatpak.get_app_commits(pkg.ref, pkg.origin, pkg.installation)
commit_idx = commits.index(pkg.commit)
@@ -133,7 +167,7 @@ class FlatpakManager(SoftwareManager):
commit = commits[commit_idx + 1]
watcher.change_substatus(self.i18n['flatpak.downgrade.reverting'])
watcher.change_progress(50)
success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, root_password),
success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, pkg.installation, root_password),
success_phrases=['Changes complete.', 'Updates complete.'],
wrong_error_phrase='Warning'))
watcher.change_progress(100)
@@ -144,10 +178,10 @@ class FlatpakManager(SoftwareManager):
self.api_cache.delete(pkg.id)
def update(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref)))
return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(pkg.ref, pkg.installation)))
def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref)))
uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref, pkg.installation)))
if self.suggestions_cache:
self.suggestions_cache.delete(pkg.id)
@@ -156,11 +190,14 @@ class FlatpakManager(SoftwareManager):
def get_info(self, app: FlatpakApplication) -> dict:
if app.installed:
app_info = flatpak.get_app_info_fields(app.id, app.branch)
app_info = flatpak.get_app_info_fields(app.id, app.branch, app.installation)
app_info['name'] = app.name
app_info['type'] = 'runtime' if app.runtime else 'app'
app_info['description'] = strip_html(app.description) if app.description else ''
if app.installation:
app_info['installation'] = app.installation
if app_info.get('installed'):
app_info['installed'] = app_info['installed'].replace('?', ' ')
@@ -193,8 +230,8 @@ class FlatpakManager(SoftwareManager):
return {}
def get_history(self, pkg: FlatpakApplication) -> PackageHistory:
pkg.commit = flatpak.get_commit(pkg.id, pkg.branch)
commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin)
pkg.commit = flatpak.get_commit(pkg.id, pkg.branch, pkg.installation)
commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin, pkg.installation)
status_idx = 0
for idx, data in enumerate(commits):
@@ -205,11 +242,56 @@ class FlatpakManager(SoftwareManager):
return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx)
def install(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool:
res = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.install(pkg.id, pkg.origin), wrong_error_phrase='Warning'))
config = read_config()
install_level = config['installation_level']
if install_level is not None:
self.logger.info("Default Flaptak installation level defined: {}".format(install_level))
if install_level not in ('user', 'system'):
watcher.show_message(title=self.i18n['error'].capitalize(),
body=self.i18n['flatpak.install.bad_install_level.body'].format(field=bold('installation_level'),
file=bold(CONFIG_FILE)),
type_=MessageType.ERROR)
return False
pkg.installation = install_level
else:
user_level = watcher.request_confirmation(title=self.i18n['flatpak.install.install_level.title'],
body=self.i18n['flatpak.install.install_level.body'].format(bold(pkg.name)),
confirmation_label=self.i18n['no'].capitalize(),
deny_label=self.i18n['yes'].capitalize())
pkg.installation = 'user' if user_level else 'system'
remotes = flatpak.list_remotes()
handler = ProcessHandler(watcher)
if pkg.installation == 'user' and not remotes['user']:
handler.handle_simple(flatpak.set_default_remotes('user'))
elif pkg.installation == 'system' and not remotes['system']:
if user.is_root():
handler.handle_simple(flatpak.set_default_remotes('system'))
else:
user_password, valid = watcher.request_root_password()
if not valid:
watcher.print('Operation aborted')
return False
else:
if not handler.handle_simple(flatpak.set_default_remotes('system', user_password)):
watcher.show_message(title=self.i18n['error'].capitalize(),
body=self.i18n['flatpak.remotes.system_flathub.error'],
type_=MessageType.ERROR)
watcher.print("Operation cancelled")
return False
res = handler.handle(SystemProcess(subproc=flatpak.install(str(pkg.id), pkg.origin, pkg.installation), wrong_error_phrase='Warning'))
if res:
try:
fields = flatpak.get_fields(pkg.id, pkg.branch, ['Ref', 'Branch'])
fields = flatpak.get_fields(str(pkg.id), pkg.branch, ['Ref', 'Branch'])
if fields:
pkg.ref = fields[0]
@@ -229,7 +311,7 @@ class FlatpakManager(SoftwareManager):
return flatpak.is_installed()
def requires_root(self, action: str, pkg: FlatpakApplication):
return action == 'downgrade'
return action == 'downgrade' and pkg.installation == 'system'
def prepare(self):
pass
@@ -253,17 +335,14 @@ class FlatpakManager(SoftwareManager):
loader.join()
for app in to_update:
updates.append(PackageUpdate(pkg_id='{}:{}'.format(app.id, app.branch),
updates.append(PackageUpdate(pkg_id='{}:{}:{}'.format(app.id, app.branch, app.installation),
pkg_type='flatpak',
version=app.version))
return updates
def list_warnings(self, internet_available: bool) -> List[str]:
if flatpak.is_installed():
if not flatpak.has_remotes_set():
return [self.i18n['flatpak.notification.no_remotes'],
self.i18n['flatpak.notification.disable'].format(bold('Flatpak'), bold(self.i18n['manage_window.settings.gems']))]
return []
def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]:
cli_version = flatpak.get_version()
@@ -277,6 +356,7 @@ class FlatpakManager(SoftwareManager):
return res
else:
self.logger.info("Mapping suggestions")
remote_level = self._get_search_remote()
installed = {i.id for i in self.read_installed(disk_loader=None).installed} if filter_installed else None
for line in file.text.split('\n'):
@@ -295,7 +375,7 @@ class FlatpakManager(SoftwareManager):
if cached_sug:
res.append(cached_sug)
else:
app_json = flatpak.search(cli_version, appid, app_id=True)
app_json = flatpak.search(cli_version, appid, remote_level, app_id=True)
if app_json:
model = PackageSuggestion(self._map_to_model(app_json[0], False, None), priority)
@@ -310,7 +390,7 @@ class FlatpakManager(SoftwareManager):
def is_default_enabled(self) -> bool:
return True
def launch(self, pkg: SoftwarePackage):
def launch(self, pkg: FlatpakApplication):
flatpak.run(str(pkg.id))
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:

View File

@@ -1,18 +1,18 @@
import re
import subprocess
import traceback
from datetime import datetime
from io import StringIO
from typing import List
from typing import List, Dict, Set
from bauh.api.exception import NoInternetException
from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess
from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess
BASE_CMD = 'flatpak'
RE_SEVERAL_SPACES = re.compile(r'\s+')
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))
def get_app_info_fields(app_id: str, branch: str, installation: str, fields: List[str] = [], check_runtime: bool = False):
info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch, installation))
data = {}
fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0)
@@ -62,12 +62,12 @@ def get_version():
return res.split(' ')[1].strip() if res else None
def get_app_info(app_id: str, branch: str):
return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def get_app_info(app_id: str, branch: str, installation: str):
return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch, '--{}'.format(installation)))
def get_commit(app_id: str, branch: str) -> str:
info = new_subprocess([BASE_CMD, 'info', app_id, branch])
def get_commit(app_id: str, branch: str, installation: str) -> str:
info = new_subprocess([BASE_CMD, 'info', app_id, branch, '--{}'.format(installation)])
for o in new_subprocess(['grep', 'Commit:', '--color=never'], stdin=info.stdout).stdout:
if o:
@@ -96,6 +96,7 @@ def list_installed(version: str) -> List[dict]:
'description': None,
'origin': data[1],
'runtime': runtime,
'installation': 'user' if 'user' in data[5] else 'system',
'version': ref_split[2] if runtime else None
})
@@ -135,52 +136,100 @@ def list_installed(version: str) -> List[dict]:
'description': data[4],
'origin': data[5],
'runtime': runtime,
'installation': 'user' if 'user' in data[6] else 'system',
'version': app_ver})
return apps
def update(app_ref: str):
def update(app_ref: str, installation: str):
"""
Updates the app reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'update', '--no-related', '-y', app_ref])
return new_subprocess([BASE_CMD, 'update', '--no-related', '--no-deps', '-y', app_ref, '--{}'.format(installation)])
def uninstall(app_ref: str):
def register_flathub(installation: str) -> SimpleProcess:
return SimpleProcess([BASE_CMD,
'remote-add',
'--if-not-exists',
'flathub',
'https://flathub.org/repo/flathub.flatpakrepo',
'--{}'.format(installation)])
def uninstall(app_ref: str, installation: str):
"""
Removes the app by its reference
:param app_ref:
:return:
"""
return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y'])
return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y', '--{}'.format(installation)])
def list_updates_as_str(version: str):
def list_updates_as_str(version: str) -> Dict[str, set]:
updates = read_updates(version, 'system')
user_updates = read_updates(version, 'user')
for attr in ('full', 'partial'):
updates[attr].update(user_updates[attr])
return updates
def read_updates(version: str, installation: str) -> Dict[str, set]:
res = {'partial': set(), 'full': set()}
if version < '1.2':
return run_cmd('{} update --no-related'.format(BASE_CMD), ignore_return_code=True)
try:
output = run_cmd('{} update --no-related --no-deps --{}'.format(BASE_CMD, installation), ignore_return_code=True)
if 'Updating in {}'.format(installation) in output:
for line in output.split('Updating in {}:\n'.format(installation))[1].split('\n'):
if not line.startswith('Is this ok'):
res['full'].add('{}/{}'.format(installation, line.split('\t')[0].strip()))
except:
traceback.print_exc()
else:
updates = new_subprocess([BASE_CMD, 'update']).stdout
updates = new_subprocess([BASE_CMD, 'update', '--{}'.format(installation)]).stdout
out = StringIO()
reg = r'[0-9]+\.\s+.+'
reg = r'[0-9]+\.\s+(\w+|\.)+\s+(\w|\.)+' if version >= '1.5.0' else r'[0-9]+\.\s+(\w+|\.)+\s+\w+\s+(\w|\.)+'
try:
for o in new_subprocess(['grep', '-E', reg, '-o', '--color=never'], stdin=updates).stdout:
if o:
line_split = o.decode().strip().split('\t')
for o in new_subprocess(['grep', '-E', reg, '-o', '--color=never'], stdin=updates).stdout:
if o:
out.write('/'.join(o.decode().strip().split('\t')[2:]) + '\n')
if version >= '1.5.0':
update_id = '{}/{}/{}'.format(line_split[2], line_split[3], installation)
else:
update_id = '{}/{}/{}'.format(line_split[2], line_split[4], installation)
out.seek(0)
return out.read()
if len(line_split) >= 6:
if line_split[4] != 'i':
if '(partial)' in line_split[-1]:
res['partial'].add(update_id)
else:
res['full'].add(update_id)
else:
res['full'].add(update_id)
except:
traceback.print_exc()
return res
def downgrade(app_ref: str, commit: str, root_password: str) -> subprocess.Popen:
return new_root_subprocess([BASE_CMD, 'update', '--no-related', '--commit={}'.format(commit), app_ref, '-y'], root_password)
def downgrade(app_ref: str, commit: str, installation: str, root_password: str) -> subprocess.Popen:
cmd = [BASE_CMD, 'update', '--no-related', '--no-deps', '--commit={}'.format(commit), app_ref, '-y', '--{}'.format(installation)]
if installation == 'system':
return new_root_subprocess(cmd, root_password)
else:
return new_subprocess(cmd)
def get_app_commits(app_ref: str, origin: str) -> List[str]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
def get_app_commits(app_ref: str, origin: str, installation: str) -> List[str]:
log = run_cmd('{} remote-info --log {} {} --{}'.format(BASE_CMD, origin, app_ref, installation))
if log:
return re.findall(r'Commit+:\s(.+)', log)
@@ -188,8 +237,8 @@ def get_app_commits(app_ref: str, origin: str) -> List[str]:
raise NoInternetException()
def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
log = run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref))
def get_app_commits_data(app_ref: str, origin: str, installation: str) -> List[dict]:
log = run_cmd('{} remote-info --log {} {} --{}'.format(BASE_CMD, origin, app_ref, installation))
if not log:
raise NoInternetException()
@@ -214,9 +263,9 @@ def get_app_commits_data(app_ref: str, origin: str) -> List[dict]:
return commits
def search(version: str, word: str, app_id: bool = False) -> List[dict]:
def search(version: str, word: str, installation: str, app_id: bool = False) -> List[dict]:
res = run_cmd('{} search {}'.format(BASE_CMD, word))
res = run_cmd('{} search {} --{}'.format(BASE_CMD, word, installation))
found = []
@@ -293,17 +342,36 @@ def search(version: str, word: str, app_id: bool = False) -> List[dict]:
return found
def install(app_id: str, origin: str):
return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y'])
def install(app_id: str, origin: str, installation: str):
return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y', '--{}'.format(installation)])
def set_default_remotes():
run_cmd('{} remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo'.format(BASE_CMD))
def set_default_remotes(installation: str, root_password: str = None) -> SimpleProcess:
cmd = [BASE_CMD, 'remote-add', '--if-not-exists', 'flathub', 'https://flathub.org/repo/flathub.flatpakrepo', '--{}'.format(installation)]
return SimpleProcess(cmd, root_password=root_password)
def has_remotes_set() -> bool:
return bool(run_cmd('{} remotes'.format(BASE_CMD)).strip())
def list_remotes() -> Dict[str, Set[str]]:
res = {'system': set(), 'user': set()}
output = run_cmd('{} remotes'.format(BASE_CMD)).strip()
if output:
lines = output.split('\n')
for line in lines:
remote = line.split('\t')
if 'system' in remote[1]:
res['system'].add(remote[0].strip())
elif 'user' in remote[1]:
res['user'].add(remote[0].strip())
return res
def run(app_id: str):
subprocess.Popen([BASE_CMD, 'run', app_id])

View File

@@ -1,13 +1,16 @@
from bauh.api.abstract.model import SoftwarePackage
import copy
from bauh.api.abstract.model import SoftwarePackage
from bauh.commons import resource
from bauh.gems.flatpak import ROOT_DIR
from bauh.view.util.translation import I18n
class FlatpakApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None,
branch: str = None, arch: str = None, origin: str = None, runtime: bool = False, ref: str = None, commit: str = None):
branch: str = None, arch: str = None, origin: str = None, runtime: bool = False, ref: str = None, commit: str = None,
installation: str = None, i18n: I18n = None):
super(FlatpakApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.ref = ref
@@ -16,6 +19,9 @@ class FlatpakApplication(SoftwarePackage):
self.origin = origin
self.runtime = runtime
self.commit = commit
self.partial = False
self.installation = installation if installation else 'system'
self.i18n = i18n
if runtime:
self.categories = ['runtime']
@@ -23,14 +29,14 @@ class FlatpakApplication(SoftwarePackage):
def is_incomplete(self):
return self.description is None and self.icon_url
def has_history(self):
return self.installed and self.ref
def has_history(self) -> bool:
return not self.partial and self.installed and self.ref
def has_info(self):
return bool(self.id)
def can_be_downgraded(self):
return self.installed and self.ref
return not self.partial and self.installed and self.ref
def get_type(self):
return 'flatpak'
@@ -63,7 +69,23 @@ class FlatpakApplication(SoftwarePackage):
setattr(self, attr, data[attr])
def can_be_run(self) -> bool:
return self.installed and not self.runtime
return self.installed and not self.runtime and not self.partial
def get_publisher(self):
return self.origin
def gen_partial(self, partial_id: str) -> "FlatpakApplication":
partial = copy.deepcopy(self)
partial.id = partial_id
if self.ref:
partial.ref = '/'.join((partial_id, *self.ref.split('/')[1:]))
partial.partial = True
return partial
def get_name_tooltip(self) -> str:
if self.installation and self.i18n is not None:
return '{} ( {} )'.format(self.name, self.i18n[self.installation.lower()])
return self.name

View File

@@ -7,6 +7,8 @@ flatpak.info.date=data
flatpak.info.description=descripció
flatpak.info.id=id
flatpak.info.installation=instal·lació
flatpak.info.installation.user=usuari
flatpak.info.installation.system=sistema
flatpak.info.installed=instal·lat
flatpak.info.license=llicència
flatpak.info.name=nom
@@ -40,3 +42,7 @@ flatpak.info.instoresincedate=afegit el
flatpak.info.projectlicense=llicència
flatpak.info.translateurl=Traducció
flatpak.info.developername=desenvolupador
flatpak.install.install_level.title=Tipus d'instal·lació
flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu ( sistema ) ?
flatpak.install.bad_install_level.body=Valor invàlid per a {field} al fitxer de configuració {file}
flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote )

View File

@@ -26,6 +26,8 @@ flatpak.info.date=Datum
flatpak.info.description=Beschreibung
flatpak.info.id=Id
flatpak.info.installation=Installation
flatpak.info.installation.user=Benutzer
flatpak.info.installation.system=System
flatpak.info.installed=installiert
flatpak.info.license=Lizenz
flatpak.info.name=Name
@@ -38,4 +40,8 @@ flatpak.info.subject=Thema
flatpak.info.type=Typ
flatpak.info.version=Version
flatpak.history.date=Datum
flatpak.history.commit=Commit
flatpak.history.commit=Commit
flatpak.install.install_level.title=Installationstyp
flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden ( system ) ?
flatpak.install.bad_install_level.body=Ungültiger Wert für {field} in der Konfigurationsdatei {file}
flatpak.remotes.system_flathub.error=Flathub konnte nicht als System-Repository ( remote ) hinzugefügt werden

View File

@@ -26,6 +26,8 @@ flatpak.info.date=date
flatpak.info.description=description
flatpak.info.id=id
flatpak.info.installation=installation
flatpak.info.installation.user=user
flatpak.info.installation.system=system
flatpak.info.installed=installed
flatpak.info.license=license
flatpak.info.name=name
@@ -38,4 +40,8 @@ flatpak.info.subject=subject
flatpak.info.type=type
flatpak.info.version=version
flatpak.history.date=date
flatpak.history.commit=commit
flatpak.history.commit=commit
flatpak.install.install_level.title=Installation type
flatpak.install.install_level.body=Should {} be installed for all the device users ( system ) ?
flatpak.install.bad_install_level.body=Invalid value for {field} in the configuration file {file}
flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote )

View File

@@ -7,6 +7,8 @@ flatpak.info.date=fecha
flatpak.info.description=descripción
flatpak.info.id=id
flatpak.info.installation=instalación
flatpak.info.installation.user=usuario
flatpak.info.installation.system=sistema
flatpak.info.installed=instalado
flatpak.info.license=licencia
flatpak.info.name=nombre
@@ -39,4 +41,8 @@ flatpak.info.homepageurl=página
flatpak.info.instoresincedate=agregado en
flatpak.info.projectlicense=licencia
flatpak.info.translateurl=Traducción
flatpak.info.developername=desarrollador
flatpak.info.developername=desarrollador
flatpak.install.install_level.title=Tipo de instalación
flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo ( sistema )?
flatpak.install.bad_install_level.body=Valor inválido para {field} en el archivo de configuración {file}
flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote )

View File

@@ -18,3 +18,10 @@ flatpak.info.instoresincedate=aggiunto in
flatpak.info.projectlicense=licenza
flatpak.info.translateurl=traduci url
flatpak.info.developername=sviluppatore
flatpak.info.installation=installazione
flatpak.info.installation.user=utente
flatpak.info.installation.system=sistema
flatpak.install.install_level.title=Tipo di installazione
flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo ( sistema ) ?
flatpak.install.bad_install_level.body=Valore non valido per {field} nel file di configurazione {file}
flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote )

View File

@@ -7,6 +7,8 @@ flatpak.info.date=data
flatpak.info.description=descrição
flatpak.info.id=id
flatpak.info.installation=instalação
flatpak.info.installation.user=usuário
flatpak.info.installation.system=sistema
flatpak.info.installed=instalado
flatpak.info.license=licença
flatpak.info.name=nome
@@ -39,4 +41,8 @@ flatpak.info.homepageurl=página
flatpak.info.instoresincedate=adicionado em
flatpak.info.projectlicense=licença
flatpak.info.translateurl=tradução
flatpak.info.developername=desenvolvedor
flatpak.info.developername=desenvolvedor
flatpak.install.install_level.title=Tipo de instalação
flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo ( sistema ) ?
flatpak.install.bad_install_level.body=Valor inválido para {field} no arquivo de configuração {file}
flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote )

View File

@@ -9,7 +9,7 @@ from bauh.gems.snap.model import SnapApplication
BASE_CMD = 'snap'
RE_SNAPD_STATUS = re.compile('\s+')
SNAPD_RUNNING_STATUS = {'listening', 'running'}
RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+')
def is_installed():
@@ -18,24 +18,23 @@ def is_installed():
def is_snapd_running() -> bool:
services = new_subprocess(['systemctl', 'list-units'])
output = run_cmd('systemctl list-units')
service, service_running = False, False
socket, socket_running = False, False
for o in new_subprocess(['grep', '-Eo', 'snapd.+'], stdin=services.stdout).stdout:
if o:
line = o.decode().strip()
snapd_services = RE_SNAPD_SERVICES.findall(output)
if line:
line_split = RE_SNAPD_STATUS.split(line)
running = line_split[3] in SNAPD_RUNNING_STATUS
socket, socket_running, service, service_running = False, False, False, False
if snapd_services:
for service_line in snapd_services:
line_split = RE_SNAPD_STATUS.split(service_line)
if line_split[0] == 'snapd.service':
service = True
service_running = running
elif line_split[0] == 'snapd.socket':
socket = True
socket_running = running
running = line_split[3] in {'listening', 'running'}
if line_split[0] == 'snapd.service':
service = True
service_running = running
elif line_split[0] == 'snapd.socket':
socket = True
socket_running = running
return socket and socket_running and (not service or service_running)

View File

@@ -9,6 +9,7 @@ from pathlib import Path
from threading import Thread
from typing import List, Type, Set, Tuple
import requests
import yaml
from colorama import Fore
from requests import exceptions
@@ -152,14 +153,14 @@ class WebApplicationManager(SoftwareManager):
def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
super(WebApplicationManager, self).serialize_to_disk(pkg=pkg, icon_bytes=None, only_icon=False)
def _map_url(self, url: str) -> "BeautifulSoup":
def _map_url(self, url: str) -> Tuple["BeautifulSoup", requests.Response]:
headers = {'Accept-language': self._get_lang_header(), 'User-Agent': UA_CHROME}
try:
url_res = self.http_client.get(url, headers=headers, ignore_ssl=True, single_call=True, session=False)
if url_res:
return BeautifulSoup(url_res.text, 'lxml', parse_only=SoupStrainer('head'))
return BeautifulSoup(url_res.text, 'lxml', parse_only=SoupStrainer('head')), url_res
except exceptions.ConnectionError as e:
self.logger.warning("Could not get {}: {}".format(url, e.__class__.__name__))
@@ -182,14 +183,21 @@ class WebApplicationManager(SoftwareManager):
if installed_matches:
res.installed.extend(installed_matches)
else:
soup = self._map_url(url)
soup_map = self._map_url(url)
if soup_map:
soup, response = soup_map[0], soup_map[1]
final_url = response.url
if final_url.endswith('/'):
final_url = final_url[0:-1]
if soup:
name = self._get_app_name(url_no_protocol, soup)
desc = self._get_app_description(url, soup)
icon_url = self._get_app_icon_url(url, soup)
desc = self._get_app_description(final_url, soup)
icon_url = self._get_app_icon_url(final_url, soup)
app = WebApplication(url=url, name=name, description=desc, icon_url=icon_url)
app = WebApplication(url=final_url, name=name, description=desc, icon_url=icon_url)
if self.env_settings.get('electron') and self.env_settings['electron'].get('version'):
app.version = self.env_settings['electron']['version']
@@ -420,7 +428,7 @@ class WebApplicationManager(SoftwareManager):
default_option=icon_op_disp if app.icon_url and app.save_icon else icon_op_ded,
label=self.i18n['web.install.option.wicon.label'])
icon_chooser = FileChooserComponent(allowed_extensions={'png', 'svg', 'ico', 'xpm'}, label=self.i18n['web.install.option.icon.label'])
icon_chooser = FileChooserComponent(allowed_extensions={'png', 'svg', 'ico', 'jpg', 'jpeg'}, label=self.i18n['web.install.option.icon.label'])
form_1 = FormComponent(components=[inp_url, inp_name, inp_desc, inp_cat, inp_icon, icon_chooser, inp_tray], label=self.i18n['web.install.options.basic'].capitalize())
@@ -515,7 +523,7 @@ class WebApplicationManager(SoftwareManager):
def _ask_update_permission(self, to_update: List[EnvironmentComponent], watcher: ProcessWatcher) -> bool:
icon = resource.get_path('img/web.svg', ROOT_DIR)
icon = resource.get_path('img/web.png', ROOT_DIR)
opts = [InputOption(label='{} ( {} )'.format(f.name, f.size or '?'),
tooltip=f.url, icon_path=icon, read_only=True, value=f.name) for f in to_update]
@@ -747,9 +755,16 @@ class WebApplicationManager(SoftwareManager):
pass
def _fill_suggestion(self, app: WebApplication):
soup = self._map_url(app.url)
soup_map = self._map_url(app.url)
if soup_map:
soup, res = soup_map[0], soup_map[1]
app.url = res.url
if app.url.endswith('/'):
app.url = app.url[0:-1]
if soup:
if not app.name:
app.name = self._get_app_name(app.url, soup)

View File

@@ -97,7 +97,7 @@ class WebApplication(SoftwarePackage):
self.set_custom_icon(self.custom_icon)
def can_be_run(self) -> bool:
return self.installed and self.installation_dir
return self.installed and bool(self.installation_dir)
def is_trustable(self) -> bool:
return False

View File

@@ -157,6 +157,7 @@ class GenericSoftwareManager(SoftwareManager):
disk_loader = None
internet_available = None
if not pkg_types: # any type
for man in self.managers:
if self._can_work(man):
@@ -164,11 +165,12 @@ class GenericSoftwareManager(SoftwareManager):
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
if thread_internet_check.isAlive():
if internet_available is None:
thread_internet_check.join()
internet_available = net_check.get('available', True)
mti = time.time()
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_check['available'])
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available)
mtf = time.time()
self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti))
@@ -185,9 +187,12 @@ class GenericSoftwareManager(SoftwareManager):
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
thread_internet_check.join()
if internet_available is None:
thread_internet_check.join()
internet_available = net_check.get('available', True)
mti = time.time()
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_check['available'])
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available)
mtf = time.time()
self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti))

View File

@@ -1,4 +1,6 @@
import operator
import os
from functools import reduce
from threading import Lock
from typing import List
@@ -6,7 +8,7 @@ from PyQt5.QtCore import Qt, QUrl, QSize
from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar, QSizePolicy
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.model import PackageStatus
@@ -14,7 +16,6 @@ from bauh.commons.html import strip_html
from bauh.view.qt import dialog
from bauh.view.qt.components import IconButton
from bauh.view.qt.view_model import PackageView, PackageViewStatus
from bauh.view.qt.view_utils import load_resource_icon
from bauh.view.util import resource
from bauh.view.util.translation import I18n
@@ -29,7 +30,7 @@ class UpdateToggleButton(QWidget):
def __init__(self, app_view: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True):
super(UpdateToggleButton, self).__init__()
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.app_view = app_view
self.root = root
@@ -74,9 +75,11 @@ class AppsTable(QTableWidget):
self.setShowGrid(False)
self.verticalHeader().setVisible(False)
self.horizontalHeader().setVisible(False)
self.horizontalHeader().setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.setSelectionBehavior(QTableView.SelectRows)
self.setHorizontalHeaderLabels(['' for _ in range(self.columnCount())])
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.icon_logo = QIcon(resource.get_path('img/logo.svg'))
self.pixmap_verified = QIcon(resource.get_path('img/verified.svg')).pixmap(QSize(10, 10))
@@ -154,7 +157,9 @@ class AppsTable(QTableWidget):
if app_v.status == PackageViewStatus.LOADING and app_v.model.status == PackageStatus.READY:
if self.download_icons:
self.network_man.get(QNetworkRequest(QUrl(app_v.model.icon_url)))
icon_request = QNetworkRequest(QUrl(app_v.model.icon_url))
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
self._update_row(app_v, change_update_col=False)
app_v.status = PackageViewStatus.READY
@@ -289,7 +294,7 @@ class AppsTable(QTableWidget):
pixmap = self.cache_type_icon.get(pkg.model.get_type())
if not pixmap:
pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(QSize(14,14))
pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(QSize(16, 16))
self.cache_type_icon[pkg.model.get_type()] = pixmap
item = QLabel()
@@ -328,7 +333,7 @@ class AppsTable(QTableWidget):
if pkg.model.name:
name = pkg.model.name
item.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), name))
item.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), pkg.model.get_name_tooltip()))
else:
name = '...'
item.setToolTip(self.i18n['app.name'].lower())
@@ -341,8 +346,9 @@ class AppsTable(QTableWidget):
item.setText(name)
if self.disk_cache and pkg.model.supports_disk_cache() and pkg.model.get_disk_icon_path() and os.path.exists(pkg.model.get_disk_icon_path()):
with open(pkg.model.get_disk_icon_path(), 'rb') as f:
icon_path = pkg.model.get_disk_icon_path()
if self.disk_cache and pkg.model.supports_disk_cache() and icon_path and os.path.isfile(icon_path):
with open(icon_path, 'rb') as f:
icon_bytes = f.read()
pixmap = QPixmap()
pixmap.loadFromData(icon_bytes)
@@ -420,32 +426,38 @@ class AppsTable(QTableWidget):
def _set_col_settings(self, col: int, pkg: PackageView):
item = QToolBar()
item.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
if pkg.model.can_be_run():
if pkg.model.installed:
def run():
self.window.run_app(pkg)
item.addWidget(IconButton(QIcon(resource.get_path('img/app_play.svg')), action=run, background='#088A08', tooltip=self.i18n['action.run.tooltip']))
bt = IconButton(QIcon(resource.get_path('img/app_play.svg')), i18n=self.i18n, action=run, background='#088A08', tooltip=self.i18n['action.run.tooltip'])
bt.setEnabled(pkg.model.can_be_run())
item.addWidget(bt)
if pkg.model.has_info():
def get_info():
self.window.get_app_info(pkg)
def get_info():
self.window.get_app_info(pkg)
bt = IconButton(QIcon(resource.get_path('img/app_info.svg')), i18n=self.i18n, action=get_info, background='#2E68D3', tooltip=self.i18n['action.info.tooltip'])
bt.setEnabled(bool(pkg.model.has_info()))
item.addWidget(bt)
item.addWidget(IconButton(QIcon(resource.get_path('img/app_info.svg')), action=get_info, background='#2E68D3', tooltip=self.i18n['action.info.tooltip']))
if pkg.model.has_screenshots():
if not pkg.model.installed:
def get_screenshots():
self.window.get_screenshots(pkg)
item.addWidget(IconButton(QIcon(resource.get_path('img/camera.svg')), action=get_screenshots, background='purple', tooltip=self.i18n['action.screenshots.tooltip']))
bt = IconButton(QIcon(resource.get_path('img/camera.svg')), i18n=self.i18n, action=get_screenshots, background='purple', tooltip=self.i18n['action.screenshots.tooltip'])
bt.setEnabled(bool(pkg.model.has_screenshots()))
item.addWidget(bt)
def handle_click():
self.show_pkg_settings(pkg)
if self.has_any_settings(pkg):
bt = IconButton(QIcon(resource.get_path('img/app_settings.svg')), action=handle_click, background='#12ABAB', tooltip=self.i18n['action.settings.tooltip'])
settings = self.has_any_settings(pkg)
if pkg.model.installed:
bt = IconButton(QIcon(resource.get_path('img/app_settings.svg')), i18n=self.i18n, action=handle_click, background='#12ABAB', tooltip=self.i18n['action.settings.tooltip'])
bt.setEnabled(bool(settings))
item.addWidget(bt)
self.setCellWidget(pkg.table_index, col, item)
@@ -454,3 +466,6 @@ class AppsTable(QTableWidget):
header_horizontal = self.horizontalHeader()
for i in range(self.columnCount()):
header_horizontal.setSectionResizeMode(i, policy)
def get_width(self):
return reduce(operator.add, [self.columnWidth(i) for i in range(self.columnCount())])

View File

@@ -241,14 +241,20 @@ class InputFilter(QLineEdit):
class IconButton(QWidget):
def __init__(self, icon: QIcon, action, background: str = None, align: int = Qt.AlignCenter, tooltip: str = None):
def __init__(self, icon: QIcon, action, i18n: I18n, background: str = None, align: int = Qt.AlignCenter, tooltip: str = None):
super(IconButton, self).__init__()
self.bt = QToolButton()
self.bt.setIcon(icon)
self.bt.clicked.connect(action)
self.i18n = i18n
self.default_tootip = tooltip
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.bt.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
if background:
self.bt.setStyleSheet('QToolButton { color: white; background: ' + background + '}')
style = 'QToolButton { color: white; background: ' + background + '} '
style += 'QToolButton:disabled { color: white; background: grey }'
self.bt.setStyleSheet(style)
if tooltip:
self.bt.setToolTip(tooltip)
@@ -259,6 +265,14 @@ class IconButton(QWidget):
layout.addWidget(self.bt)
self.setLayout(layout)
def setEnabled(self, enabled):
super(IconButton, self).setEnabled(enabled)
if not enabled:
self.bt.setToolTip(self.i18n['icon_button.tooltip.disabled'])
else:
self.bt.setToolTip(self.default_tootip)
class FormQt(QGroupBox):
@@ -319,7 +333,7 @@ class FormQt(QGroupBox):
options = QFileDialog.Options()
if c.allowed_extensions:
exts = ';;'.join({'{} {} (*.{})'.format(self.i18n['files'].capitalize(), e.upper(), e) for e in c.allowed_extensions})
exts = ';;'.join({'*.{}'.format(e) for e in c.allowed_extensions})
else:
exts = '{}} (*);;'.format(self.i18n['all_files'].capitalize())

View File

@@ -1,12 +1,12 @@
from typing import List
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget, QScrollArea, QFrame, QSizePolicy
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget, QScrollArea, QFrame
from bauh.api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent, TextInputComponent, \
FormComponent
from bauh.view.qt import css
from bauh.view.qt.components import MultipleSelectQt, new_single_select, TextInputQt, FormQt, new_spacer
from bauh.view.qt.components import MultipleSelectQt, new_single_select, TextInputQt, FormQt
from bauh.view.util.translation import I18n
@@ -19,6 +19,7 @@ class ConfirmationDialog(QMessageBox):
self.setStyleSheet('QLabel { margin-right: 25px; }')
self.bt_yes = self.addButton(i18n['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole)
self.bt_yes.setStyleSheet(css.OK_BUTTON)
self.setDefaultButton(self.bt_yes)
self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole)

View File

@@ -45,6 +45,7 @@ def ask_confirmation(title: str, body: str, i18n: I18n, icon: QIcon = QIcon(reso
bt_yes = diag.addButton(i18n['popup.button.yes'], QMessageBox.YesRole)
bt_yes.setStyleSheet(css.OK_BUTTON)
diag.setDefaultButton(bt_yes)
diag.addButton(i18n['popup.button.no'], QMessageBox.NoRole)

View File

@@ -53,7 +53,7 @@ class ScreenshotsDialog(QDialog):
self.bottom_bar = QToolBar()
self.bt_back = QPushButton(self.i18n['screenshots.bt_back.label'].capitalize())
self.bt_back = QPushButton(' < ' + self.i18n['screenshots.bt_back.label'].capitalize())
self.bt_back.clicked.connect(self.back)
self.ref_bt_back = self.bottom_bar.addWidget(self.bt_back)
self.bottom_bar.addWidget(new_spacer(50))
@@ -65,7 +65,7 @@ class ScreenshotsDialog(QDialog):
self.ref_progress_bar = self.bottom_bar.addWidget(self.progress_bar)
self.bottom_bar.addWidget(new_spacer(50))
self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize())
self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize() + ' > ')
self.bt_next.clicked.connect(self.next)
self.ref_bt_next = self.bottom_bar.addWidget(self.bt_next)

View File

@@ -1,7 +1,7 @@
import re
import time
from datetime import datetime, timedelta
from typing import List, Type, Set
from typing import List, Type, Set, Tuple
import requests
from PyQt5.QtCore import QThread, pyqtSignal
@@ -28,11 +28,13 @@ class AsyncAction(QThread, ProcessWatcher):
signal_status = pyqtSignal(str) # changes the GUI status message
signal_substatus = pyqtSignal(str) # changes the GUI substatus message
signal_progress = pyqtSignal(int)
signal_root_password = pyqtSignal()
def __init__(self):
super(AsyncAction, self).__init__()
self.wait_confirmation = False
self.confirmation_res = None
self.root_password = None
self.stop = False
def request_confirmation(self, title: str, body: str, components: List[InputViewComponent] = None, confirmation_label: str = None, deny_label: str = None) -> bool:
@@ -41,10 +43,22 @@ class AsyncAction(QThread, ProcessWatcher):
self.wait_user()
return self.confirmation_res
def request_root_password(self) -> Tuple[str, bool]:
self.wait_confirmation = True
self.signal_root_password.emit()
self.wait_user()
res = self.root_password
self.root_password = None
return res
def confirm(self, res: bool):
self.confirmation_res = res
self.wait_confirmation = False
def set_root_password(self, password: str, valid: bool):
self.root_password = (password, valid)
self.wait_confirmation = False
def wait_user(self):
while self.wait_confirmation:
time.sleep(0.01)

View File

@@ -1,12 +1,10 @@
import logging
import operator
import time
from functools import reduce
from pathlib import Path
from typing import List, Type, Set
from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap, QCursor
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QMenu, QAction, QApplication, QListView
@@ -16,9 +14,10 @@ from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.api.abstract.view import MessageType
from bauh.api.http import HttpClient
from bauh.commons import user
from bauh.commons.html import bold
from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.qt import dialog, commons, qt_utils
from bauh.view.qt import dialog, commons, qt_utils, root
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton
from bauh.view.qt.components import new_spacer, InputFilter, IconButton
@@ -26,14 +25,14 @@ from bauh.view.qt.confirmation import ConfirmationDialog
from bauh.view.qt.gem_selector import GemSelectorPanel
from bauh.view.qt.history import HistoryDialog
from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import is_root, ask_root_password
from bauh.view.qt.root import ask_root_password
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.qt.styles import StylesComboBox
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
from bauh.view.qt.view_model import PackageView
from bauh.view.qt.view_utils import load_icon, load_resource_icon
from bauh.view.qt.view_utils import load_icon
from bauh.view.util import util, resource
from bauh.view.util.translation import I18n
@@ -48,6 +47,7 @@ class ManageWindow(QWidget):
__BASE_HEIGHT__ = 400
signal_user_res = pyqtSignal(bool)
signal_root_password = pyqtSignal(str, bool)
signal_table_update = pyqtSignal()
def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size, config: dict,
@@ -118,8 +118,7 @@ class ManageWindow(QWidget):
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.setPixmap(QIcon(resource.get_path('img/search.svg')).pixmap(QSize(10,10)))
label_pos_search.setPixmap(QIcon(resource.get_path('img/search.svg')).pixmap(QSize(10, 10)))
label_pos_search.setStyleSheet("""
background: white; padding-right: 10px;
border-top-right-radius: 5px;
@@ -158,7 +157,6 @@ class ManageWindow(QWidget):
self.combo_filter_type.setEditable(True)
self.combo_filter_type.lineEdit().setReadOnly(True)
self.combo_filter_type.lineEdit().setAlignment(Qt.AlignCenter)
self.combo_filter_type.setIconSize(QSize(14,14))
self.combo_filter_type.activated.connect(self._handle_type_filter)
self.combo_filter_type.addItem('--- {} ---'.format(self.i18n['type'].capitalize()), self.any_type_filter)
self.ref_combo_filter_type = self.toolbar.addWidget(self.combo_filter_type)
@@ -324,6 +322,7 @@ class ManageWindow(QWidget):
bt_settings = IconButton(QIcon(resource.get_path('img/app_settings.svg')),
action=self._show_settings_menu,
background='#12ABAB',
i18n=self.i18n,
tooltip=self.i18n['manage_window.bt_settings.tooltip'])
self.ref_bt_settings = self.toolbar_bottom.addWidget(bt_settings)
@@ -395,8 +394,10 @@ class ManageWindow(QWidget):
action.signal_status.connect(self._change_label_status)
action.signal_substatus.connect(self._change_label_substatus)
action.signal_progress.connect(self._update_process_progress)
action.signal_root_password.connect(self._ask_root_password)
self.signal_user_res.connect(action.confirm)
self.signal_root_password.connect(action.set_root_password)
return action
@@ -413,6 +414,12 @@ class ManageWindow(QWidget):
self.thread_animate_progress.animate()
self.signal_user_res.emit(res)
def _ask_root_password(self):
self.thread_animate_progress.pause()
password, valid = root.ask_root_password(self.i18n)
self.thread_animate_progress.animate()
self.signal_root_password.emit(password, valid)
def _show_message(self, msg: dict):
self.thread_animate_progress.pause()
dialog.show_message(title=msg['title'], body=msg['body'], type_=msg['type'])
@@ -548,7 +555,7 @@ class ManageWindow(QWidget):
pwd = None
requires_root = self.manager.requires_root('uninstall', app.model)
if not is_root() and requires_root:
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
@@ -780,7 +787,7 @@ class ManageWindow(QWidget):
icon = self.cache_type_filter_icons.get(app_type)
if not icon:
icon = QIcon(icon_path)
icon = load_icon(icon_path, 14)
self.cache_type_filter_icons[app_type] = icon
self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type)
@@ -831,20 +838,16 @@ class ManageWindow(QWidget):
self.ref_combo_categories.setVisible(False)
def resize_and_center(self, accept_lower_width: bool = True):
if self.pkgs:
new_width = reduce(operator.add, [self.table_apps.columnWidth(i) for i in range(self.table_apps.columnCount())])
table_width = self.table_apps.get_width()
toolbar_width = self.toolbar.sizeHint().width()
topbar_width = self.toolbar_top.sizeHint().width()
if self.ref_bt_upgrade.isVisible() or self.ref_bt_settings.isVisible():
new_width *= 1.07
else:
new_width = self.toolbar_top.width()
new_width = max(table_width, toolbar_width, topbar_width)
new_width *= 1.05 # this extra size is not because of the toolbar button, but the table upgrade buttons
if accept_lower_width or new_width > self.width():
if (self.pkgs and accept_lower_width) or new_width > self.width():
self.resize(new_width, self.height())
if self.ref_bt_upgrade.isVisible() and self.bt_upgrade.visibleRegion().isEmpty():
self.adjustSize()
qt_utils.centralize(self)
def update_selected(self):
@@ -866,7 +869,7 @@ class ManageWindow(QWidget):
widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]):
pwd = None
if not is_root() and requires_root:
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
@@ -984,7 +987,7 @@ class ManageWindow(QWidget):
pwd = None
requires_root = self.manager.requires_root('downgrade', pkgv.model)
if not is_root() and requires_root:
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
@@ -1077,7 +1080,7 @@ class ManageWindow(QWidget):
pwd = None
requires_root = self.manager.requires_root('install', pkg.model)
if not is_root() and requires_root:
if not user.is_root() and requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:
@@ -1132,7 +1135,7 @@ class ManageWindow(QWidget):
def execute_custom_action(self, pkg: PackageView, action: PackageAction):
pwd = None
if not is_root() and action.requires_root:
if not user.is_root() and action.requires_root:
pwd, ok = ask_root_password(self.i18n)
if not ok:

View File

@@ -48,7 +48,7 @@ popup.history.selected.tooltip=Versió actual
popup.button.yes=Sí
popup.button.no=No
popup.button.cancel=Cancel·la
tray.action.manage=Gestiona les aplicacions
tray.action.manage=Aplicacions
tray.action.exit=Surt
tray.action.about=Quant a
tray.action.refreshing=Refrescant
@@ -217,4 +217,6 @@ files=fitxers
all_files=tots els fitxers
file_chooser.title=Selector de fitxers
message.file.not_exist=Fitxer no existeix
message.file.not_exist.body=El fitxer {} sembla no existir
message.file.not_exist.body=El fitxer {} sembla no existir
icon_button.tooltip.disabled=Aquesta acció no està disponible
user=usuari

View File

@@ -48,7 +48,7 @@ popup.history.selected.tooltip=Aktuelle Version
popup.button.yes=Ja
popup.button.no=Nein
popup.button.cancel=Abbrechen
tray.action.manage=Anwendungen verwalten
tray.action.manage=Anwendungen
tray.action.exit=Beenden
tray.action.about=Über
tray.action.refreshing=Aktualisieren
@@ -172,4 +172,6 @@ all_files=Alle Dateien
file_chooser.title=Dateiauswahl
message.file.not_exist=Datei existiert nicht
message.file.not_exist.body=Die Datei {} scheint nicht zu existieren
development=Entwicklung
development=Entwicklung
icon_button.tooltip.disabled=Diese Aktion ist nicht verfügbar
user=Benutzer

View File

@@ -4,9 +4,9 @@ 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.popup.title=Uninstall
manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} from your computer ?
manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} from your device ?
manage_window.apps_table.row.actions.install.popup.title=Installation
manage_window.apps_table.row.actions.install.popup.body=Install {} on your computer ?
manage_window.apps_table.row.actions.install.popup.body=Install {} on your device ?
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
@@ -48,7 +48,7 @@ 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.manage=Applications
tray.action.exit=Quit
tray.action.about=About
tray.action.refreshing=Refreshing
@@ -176,4 +176,6 @@ all_files=all files
file_chooser.title=File selector
message.file.not_exist=File does not exist
message.file.not_exist.body=The file {} seems not to exist
development=development
development=development
icon_button.tooltip.disabled=This action is unavailable
user=user

View File

@@ -49,7 +49,7 @@ popup.history.selected.tooltip=Versión actual
popup.button.yes=Sí
popup.button.no=No
popup.button.cancel=Cancelar
tray.action.manage=Administrar aplicaciones
tray.action.manage=Aplicaciones
tray.action.exit=Cerrar
tray.action.about=Acerca de
tray.action.refreshing=Recargando
@@ -216,4 +216,6 @@ files=archivos
all_files=todos los archivos
file_chooser.title=Selector de archivos
message.file.not_exist=Archivo no existe
message.file.not_exist.body=El archivo {} parece no existir
message.file.not_exist.body=El archivo {} parece no existir
icon_button.tooltip.disabled=This action is unavailable
user=usuario

View File

@@ -4,9 +4,9 @@ manage_window.columns.update=Upgrade ?
manage_window.apps_table.row.actions.info=Informazioni
manage_window.apps_table.row.actions.history=Cronologia
manage_window.apps_table.row.actions.uninstall.popup.title=Disinstalla
manage_window.apps_table.row.actions.uninstall.popup.body=Remuovi {} dal tuo computer ?
manage_window.apps_table.row.actions.uninstall.popup.body=Remuovi {} dal tuo dispositivo ?
manage_window.apps_table.row.actions.install.popup.title=Installazione
manage_window.apps_table.row.actions.install.popup.body=Installa {} nel tuo computer ?
manage_window.apps_table.row.actions.install.popup.body=Installa {} nel tuo dispositivo ?
manage_window.apps_table.row.actions.downgrade=Downgrade
manage_window.apps_table.row.actions.downgrade.popup.body=Vuoi veramente fare il downgrade {} ?
manage_window.apps_table.row.actions.install=Installa
@@ -48,7 +48,7 @@ popup.history.selected.tooltip=Version corrente
popup.button.yes=Si
popup.button.no=No
popup.button.cancel=Cancella
tray.action.manage=Gestisci applicazioni
tray.action.manage=Applicazioni
tray.action.exit=Abbandona
tray.action.about=Informazioni su
tray.action.refreshing=Refreshing
@@ -173,4 +173,6 @@ all_files=tutti i files
file_chooser.title=Selettore file
message.file.not_exist=File non esiste
message.file.not_exist.body=Il file {} sembra non esistere
development=sviluppo
development=sviluppo
icon_button.tooltip.disabled=Questa azione non è disponibile
user=utente

View File

@@ -6,9 +6,9 @@ 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 {} do seu computador ?
manage_window.apps_table.row.actions.uninstall.popup.body=Remover {} do seu dispositivo ?
manage_window.apps_table.row.actions.install.popup.title=Instalação
manage_window.apps_table.row.actions.install.popup.body=Instalar {} no seu computador ?
manage_window.apps_table.row.actions.install.popup.body=Instalar {} no seu dispositivo ?
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
@@ -49,7 +49,7 @@ 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.manage=Aplicativos
tray.action.exit=Fechar
tray.action.about=Sobre
tray.action.refreshing=Recarregando
@@ -219,4 +219,6 @@ files=arquivos
all_files=todos os arquivos
file_chooser.title=Seletor arquivos
message.file.not_exist=Arquivo não existe
message.file.not_exist.body=O arquivo {} parece não existir
message.file.not_exist.body=O arquivo {} parece não existir
icon_button.tooltip.disabled=Esta ação está indisponível
user=usuário