diff --git a/CHANGELOG.md b/CHANGELOG.md index 57501c34..c3d9bae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index afa95cb8..8ee60b06 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/bauh/api/abstract/handler.py b/bauh/api/abstract/handler.py index ffdc52c6..3f8ec64e 100644 --- a/bauh/api/abstract/handler.py +++ b/bauh/api/abstract/handler.py @@ -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 + """ \ No newline at end of file diff --git a/bauh/api/abstract/model.py b/bauh/api/abstract/model.py index 7bcc2781..1500b3f9 100644 --- a/bauh/api/abstract/model.py +++ b/bauh/api/abstract/model.py @@ -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) diff --git a/bauh/app.py b/bauh/app.py index 01602d06..460b3688 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -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 diff --git a/bauh/commons/user.py b/bauh/commons/user.py new file mode 100644 index 00000000..6b5a0f63 --- /dev/null +++ b/bauh/commons/user.py @@ -0,0 +1,5 @@ +import os + + +def is_root(): + return os.getuid() == 0 diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 538d3051..9a4543d0 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -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))) diff --git a/bauh/gems/flatpak/__init__.py b/bauh/gems/flatpak/__init__.py index b39f2445..9d497cb2 100644 --- a/bauh/gems/flatpak/__init__.py +++ b/bauh/gems/flatpak/__init__.py @@ -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()) diff --git a/bauh/gems/flatpak/config.py b/bauh/gems/flatpak/config.py new file mode 100644 index 00000000..c6c9dcff --- /dev/null +++ b/bauh/gems/flatpak/config.py @@ -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) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index f4a367aa..f97fe971 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -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]: diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index ba9d40a4..471898de 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -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]) diff --git a/bauh/gems/flatpak/model.py b/bauh/gems/flatpak/model.py index 4a95c7ff..880f54e0 100644 --- a/bauh/gems/flatpak/model.py +++ b/bauh/gems/flatpak/model.py @@ -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 diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index 93c461c9..10f50234 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -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 ) \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index f04d5bcb..dc60e06b 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index e89558d8..4176e3cf 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -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 \ No newline at end of file +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 ) \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index 9a276d5a..2aaa0e92 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -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 \ No newline at end of file +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 ) \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index f8c9b630..42521925 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -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 ) \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index 986bfdd3..bd1a5301 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -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 \ No newline at end of file +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 ) \ No newline at end of file diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index d17e1c0b..badace69 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -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) diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 45fde8db..d58db812 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -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) diff --git a/bauh/gems/web/model.py b/bauh/gems/web/model.py index be82babc..572e3127 100644 --- a/bauh/gems/web/model.py +++ b/bauh/gems/web/model.py @@ -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 diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 5f28f914..9f2f4075 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -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)) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 19acbc11..985020c7 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -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())]) diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index dd7b2da2..847ac0ae 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -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()) diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index edd2b04e..c797509e 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -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) diff --git a/bauh/view/qt/dialog.py b/bauh/view/qt/dialog.py index 2dbc1a08..bbe0338e 100644 --- a/bauh/view/qt/dialog.py +++ b/bauh/view/qt/dialog.py @@ -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) diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py index f61712d3..62f4bfdc 100644 --- a/bauh/view/qt/screenshots.py +++ b/bauh/view/qt/screenshots.py @@ -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) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index a3edd855..24d81103 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -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) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 536a59e0..50c4bd7a 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -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: diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 965fe179..3c25ab48 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -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 \ No newline at end of file +message.file.not_exist.body=El fitxer {} sembla no existir +icon_button.tooltip.disabled=Aquesta acció no està disponible +user=usuari \ No newline at end of file diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index d931929d..ecadbca5 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -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 \ No newline at end of file +development=Entwicklung +icon_button.tooltip.disabled=Diese Aktion ist nicht verfügbar +user=Benutzer \ No newline at end of file diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 079441d4..24113690 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -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 \ No newline at end of file +development=development +icon_button.tooltip.disabled=This action is unavailable +user=user \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index ba5dfe81..e79ac6fb 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -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 \ No newline at end of file +message.file.not_exist.body=El archivo {} parece no existir +icon_button.tooltip.disabled=This action is unavailable +user=usuario \ No newline at end of file diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index d9d9fed0..75967b75 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -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 \ No newline at end of file +development=sviluppo +icon_button.tooltip.disabled=Questa azione non è disponibile +user=utente \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 55e63265..15390715 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -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 \ No newline at end of file +message.file.not_exist.body=O arquivo {} parece não existir +icon_button.tooltip.disabled=Esta ação está indisponível +user=usuário \ No newline at end of file