diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e382edc..c064aa3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,20 @@ 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] +### Features: +- Flatpak: + - allow the user to choose the application installation level: **user** or **system** + - able to deal with user and system applications / runtimes + - 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 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 - Web: - 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 @@ -16,17 +27,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - scaling ### Fixes -- missing categories i18n [48](https://github.com/vinifmor/bauh/issues/48) +- 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 ( an exception happens for Python 3.5 ) - UI: - not verifying if an icon path is a file - minor fixes -- minor bug 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/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..b384f09e 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,19 +118,36 @@ 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: @@ -121,7 +155,7 @@ class FlatpakManager(SoftwareManager): 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) @@ -161,6 +195,9 @@ class FlatpakManager(SoftwareManager): 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('?', ' ') @@ -194,7 +231,7 @@ class FlatpakManager(SoftwareManager): 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) + 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..d661a14d 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -1,11 +1,12 @@ 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, SystemProcess BASE_CMD = 'flatpak' RE_SEVERAL_SPACES = re.compile(r'\s+') @@ -96,6 +97,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 +137,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 +238,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 +264,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 +343,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/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/qt/apps_table.py b/bauh/view/qt/apps_table.py index 698263bb..985020c7 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -333,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()) @@ -428,31 +428,36 @@ class AppsTable(QTableWidget): 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) diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index b0eec544..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): 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 9acc76fc..32ebc393 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -1,7 +1,5 @@ import logging -import operator import time -from functools import reduce from pathlib import Path from typing import List, Type, Set @@ -9,7 +7,7 @@ from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal 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, \ - QSizePolicy, QMainWindow + QSizePolicy from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext @@ -17,9 +15,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 @@ -27,7 +26,7 @@ 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, \ @@ -49,6 +48,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, @@ -324,6 +324,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 +396,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 +416,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 +557,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: @@ -838,13 +847,12 @@ class ManageWindow(QWidget): new_width = max(table_width, toolbar_width, topbar_width) if self.bt_upgrade.isVisible(): - new_width *= 1.03 # this extra size is not because of the toolbar button, but the table upgrade buttons + new_width *= 1.05 # this extra size is not because of the toolbar button, but the table upgrade buttons if (self.pkgs and accept_lower_width) or new_width > self.width(): self.resize(new_width, self.height()) - if self.first_refresh: - qt_utils.centralize(self) + qt_utils.centralize(self) def update_selected(self): if self.pkgs: @@ -865,7 +873,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: @@ -983,7 +991,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: @@ -1076,7 +1084,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: @@ -1131,7 +1139,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..fc3565f7 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -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..d61f0295 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -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..2c30bb91 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -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..d3b46dd1 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -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..7fec700b 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -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..bd82a25c 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -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