diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d9bae7..13a9b8fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,56 @@ 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.2] - 2020-01-31 +### Features +- New **Settings** panel ( displayed when the lower **Settings** button is clicked ). It allows to change all settings. + +### Improvements +- Flatpak + - configuration file ( **flatpak.yml** ) will be created during the initialization ( on **0.8.1** it would only be created during the first app installation ) +- AUR + - the custom **makepkg.conf** generated at **~/.config/bauh/arch** will enable **ccache** if available on the system + - downgrading time reduced due to the fix described in ***Fixes*** + - package databases synchronization once a day ( or every device reboot ) before the first package installation / upgrade / downgrade. This behavior can be disabled on **~/.config/arch.yml** / or the new settings panel + ``` + sync_databases: true # enabled by default + ``` +- Configuration ( **~/.config/bauh/config.yml** ) + - new property **hdpi** allowing to disable HDPI improvements + ``` + ui: + hdpi: true # enabled by default + ``` + - new property **auto_scale** activates Qt auto screen scale factor ( **QT_AUTO_SCREEN_SCALE_FACTOR** ). It fixes scaling issues + for some desktop environments ( like Gnome ) [#1](https://github.com/vinifmor/bauh/issues/1) + ``` + ui: + auto_scale: false # disabled by default + ``` +### Fixes +- AUR + - not treating **makedepends** as a list during dependency checking ( **anbox-git** installation was crashing ) + - not considering the package name itself as **provided** during dependency checking ( **anbox-git** installation was crashing ) + - not pre-downloading some source files ( e.g: from **anbox-image** ) + - not able to install packages based on other packages ( package name != package base ). e.g: **anbox-modules-dkms-git** > **anbox-git** + - downgrade: pre-downloading sources from the latest version instead of the older +- Flatpak + - downgrade: displaying "No Internet connection" when an error happens during commits reading + - Flatpak < 1.5: an exception happens when trying to retrieve the information from partials +- UI: + - **About** window icons scaling + - Toolbar buttons get hidden [#5](https://github.com/vinifmor/bauh/issues/5) + - not displaying icons retrieved from a HTTP redirect + - minor bug fixes + +### UI +- **Style selector** and **Application types** menu action moved to the new **Settings panel** +- **About** menu action split from the **Settings** menu as a new button +- The file chooser component now has a clean button alongside + ## [0.8.1] 2020-01-14 -### Features: -- Flatpak: +### 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 @@ -15,16 +62,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Improvements - All icons are now SVG files - HDPI support improvements ( by [octopusSD](https://github.com/octopusSD) ) -- Flatpak: +- 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: +- Snap - snapd checking routine refactored -- Web: +- 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 -- UI: +- UI - widgets visibility settings: the main widgets now should always be visible ( e.g: toolbar buttons ) - scaling diff --git a/README.md b/README.md index de95a480..197b7b07 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ db_updater: ``` optimize: true # if 'false': disables the auto-compilation improvements transitive_checking: true # if 'false': the dependency checking process will be faster, but the application will ask for a confirmation every time a not installed dependency is detected. +sync_databases: true # package databases synchronization once a day ( or every device reboot ) before the first package installation / upgrade / downgrade ``` - Required dependencies: - **pacman** @@ -242,6 +243,8 @@ ui: tray: # system tray settings default_icon: null # defines a path to a custom icon updates_icon: null # defines a path to a custom icon indicating updates + hdpi: true # enables HDPI rendering improvements. Use 'false' to disable them if you think the interface looks strange + auto_scale: false # activates Qt auto screen scale factor (QT_AUTO_SCREEN_SCALE_FACTOR). It fixes scaling issues for some desktop environments ( like Gnome ) updates: check_interval: 30 # the updates checking interval in SECONDS diff --git a/bauh/__init__.py b/bauh/__init__.py index e6a21914..a7c7df35 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.8.1' +__version__ = '0.8.2' __app_name__ = 'bauh' import os diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py index 69e606de..4fcc5adb 100644 --- a/bauh/api/abstract/context.py +++ b/bauh/api/abstract/context.py @@ -12,7 +12,7 @@ class ApplicationContext: def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n, cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory, - logger: logging.Logger, file_downloader: FileDownloader, distro: str): + logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str): """ :param disk_cache: if package data should be cached to disk :param download_icons: if packages icons should be downloaded @@ -24,6 +24,7 @@ def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClie :param logger: a logger instance :param file_downloader :param distro + :param app_name """ self.disk_cache = disk_cache self.download_icons = download_icons @@ -38,6 +39,7 @@ def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClie self.distro = distro self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game', 'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility') + self.app_name = app_name def is_system_x86_64(self): return self.arch_x86_64 diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 3876d0a9..0bea45ac 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -3,7 +3,7 @@ import shutil from abc import ABC, abstractmethod from pathlib import Path -from typing import List, Set, Type +from typing import List, Set, Type, Tuple import yaml @@ -11,6 +11,7 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction +from bauh.api.abstract.view import FormComponent, ViewComponent class SearchResult: @@ -272,3 +273,17 @@ def clear_data(self): Removes all data created by the SoftwareManager instance """ pass + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + """ + :param screen_width + :param screen_height + :return: a form abstraction with all available settings + """ + pass + + def save_settings(self, component: ViewComponent) -> Tuple[bool, List[str]]: + """ + :return: a tuple with a bool informing if the settings were saved and a list of error messages + """ + pass diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 8242fa9d..865563fa 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -17,6 +17,24 @@ def __init__(self, id_: str): self.id = id_ +class SpacerComponent(ViewComponent): + + def __init__(self): + super(SpacerComponent, self).__init__(id_=None) + + +class PanelComponent(ViewComponent): + + def __init__(self, components: List[ViewComponent], id_: str = None): + super(PanelComponent, self).__init__(id_=id_) + self.components = components + self.component_map = {c.id: c for c in components if c.id is not None} if components else None + + def get_component(self, id_: str) -> ViewComponent: + if self.component_map: + return self.component_map.get(id_) + + class InputViewComponent(ViewComponent): """ Represents an component which needs a user interaction to provide its value @@ -38,9 +56,6 @@ def __init__(self, label: str, value: object, tooltip: str = None, icon_path: st if not label: raise Exception("'label' must be a not blank string") - if value is None: - raise Exception("'value' must be a not blank string") - self.id = id_ self.label = label self.value = value @@ -59,13 +74,16 @@ class SelectViewType(Enum): class SingleSelectComponent(InputViewComponent): - def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, max_per_line: int = 1, id_: str = None): + def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, + max_per_line: int = 1, tooltip: str = None, max_width: int = -1, id_: str = None): super(SingleSelectComponent, self).__init__(id_=id_) self.type = type_ self.label = label self.options = options self.value = default_option self.max_per_line = max_per_line + self.tooltip = tooltip + self.max_width = max_width def get_selected(self): if self.value: @@ -74,16 +92,22 @@ def get_selected(self): class MultipleSelectComponent(InputViewComponent): - def __init__(self, label: str, options: List[InputOption], default_options: Set[InputOption] = None, max_per_line: int = 1, id_: str = None): + def __init__(self, label: str, options: List[InputOption], default_options: Set[InputOption] = None, + max_per_line: int = 1, tooltip: str = None, spaces: bool = True, max_width: int = -1, + max_height: int = -1, id_: str = None): super(MultipleSelectComponent, self).__init__(id_=id_) if not options: raise Exception("'options' cannot be None or empty") self.options = options + self.spaces = spaces self.label = label + self.tooltip = tooltip self.values = default_options if default_options else set() self.max_per_line = max_per_line + self.max_width = max_width + self.max_height = max_height def get_selected_values(self) -> list: selected = [] @@ -95,20 +119,34 @@ def get_selected_values(self) -> list: class TextComponent(ViewComponent): - def __init__(self, html: str, id_: str = None): + def __init__(self, html: str, max_width: int = -1, tooltip: str = None, id_: str = None): super(TextComponent, self).__init__(id_=id_) self.value = html + self.max_width = max_width + self.tooltip = tooltip + + +class TwoStateButtonComponent(ViewComponent): + + def __init__(self, label: str, tooltip: str = None, state: bool = False, id_: str = None): + super(TwoStateButtonComponent, self).__init__(id_=id_) + self.label = label + self.tooltip = tooltip + self.state = state class TextInputComponent(ViewComponent): - def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, id_: str = None): + def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, + id_: str = None, only_int: bool = False, max_width: int = -1): super(TextInputComponent, self).__init__(id_=id_) self.label = label self.value = value self.tooltip = tooltip self.placeholder = placeholder self.read_only = read_only + self.only_int = only_int + self.max_width = max_width def get_value(self) -> str: if self.value is not None: @@ -116,19 +154,54 @@ def get_value(self) -> str: else: return '' + def get_int_value(self) -> int: + if self.value is not None: + return int(self.value) + return None + class FormComponent(ViewComponent): - def __init__(self, components: List[ViewComponent], label: str = None, id_: str = None): + def __init__(self, components: List[ViewComponent], label: str = None, spaces: bool = True, id_: str = None): super(FormComponent, self).__init__(id_=id_) self.label = label + self.spaces = spaces self.components = components + self.component_map = {c.id: c for c in components if c.id} if components else None + + def get_component(self, id_: str) -> ViewComponent: + if self.component_map: + return self.component_map.get(id_) class FileChooserComponent(ViewComponent): - def __init__(self, allowed_extensions: Set[str] = None, label: str = None, id_: str = None): + def __init__(self, allowed_extensions: Set[str] = None, label: str = None, tooltip: str = None, + file_path: str = None, max_width: int = -1, id_: str = None): super(FileChooserComponent, self).__init__(id_=id_) self.label = label self.allowed_extensions = allowed_extensions - self.file_path = None + self.file_path = file_path + self.tooltip = tooltip + self.max_width = max_width + + +class TabComponent(ViewComponent): + + def __init__(self, label: str, content: ViewComponent, icon_path: str = None, id_: str = None): + super(TabComponent, self).__init__(id_=id_) + self.label = label + self.content = content + self.icon_path = icon_path + + +class TabGroupComponent(ViewComponent): + + def __init__(self, tabs: List[TabComponent], id_: str = None): + super(TabGroupComponent, self).__init__(id_=id_) + self.tabs = tabs + self.tab_map = {c.id: c for c in tabs if c.id} if tabs else None + + def get_tab(self, id_: str) -> TabComponent: + if self.tab_map: + return self.tab_map.get(id_) diff --git a/bauh/app.py b/bauh/app.py index 460b3688..8a27c9b0 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -3,7 +3,7 @@ from threading import Thread import urllib3 -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QCoreApplication from PyQt5.QtWidgets import QApplication from bauh import __version__, __app_name__, app_args, ROOT_DIR @@ -31,10 +31,18 @@ def main(): args = app_args.read() logger = logs.new_logger(__app_name__, bool(args.logs)) - app_args.validate(args, logger) local_config = config.read_config(update_file=True) + if local_config['ui']['auto_scale']: + os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1' + logger.info("Auto screen scale factor activated") + + if local_config['ui']['hdpi']: + logger.info("HDPI settings activated") + QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + i18n_key, current_i18n = translation.get_locale_keys(local_config['locale']) default_i18n = translation.get_locale_keys(DEFAULT_I18N_KEY)[1] if i18n_key != DEFAULT_I18N_KEY else {} i18n = I18n(i18n_key, current_i18n, DEFAULT_I18N_KEY, default_i18n) @@ -55,7 +63,8 @@ def main(): logger=logger, distro=util.get_distro(), file_downloader=AdaptableFileDownloader(logger, bool(local_config['download']['multithreaded']), - i18n, http_client)) + i18n, http_client), + app_name=__app_name__) managers = gems.load_managers(context=context, locale=i18n_key, config=local_config, default_locale=DEFAULT_I18N_KEY) @@ -71,7 +80,6 @@ def main(): app.setApplicationVersion(__version__) app_icon = util.get_default_icon()[1] app.setWindowIcon(app_icon) - app.setAttribute(Qt.AA_UseHighDpiPixmaps) # This fix images on HDPI resolution, not tested on non HDPI if local_config['ui']['style']: app.setStyle(str(local_config['ui']['style'])) diff --git a/bauh/app_args.py b/bauh/app_args.py index 19d2b898..52fbdbfe 100644 --- a/bauh/app_args.py +++ b/bauh/app_args.py @@ -15,11 +15,3 @@ def read() -> Namespace: parser.add_argument('--show-panel', action="store_true", help='Shows the management panel after the app icon is attached to the tray.') parser.add_argument('--reset', action="store_true", help='Removes all configuration and cache files') return parser.parse_args() - - -def validate(args: Namespace, logger: logging.Logger): - - if args.logs == 1: - logger.info("Logs are enabled") - - return args diff --git a/bauh/gems/appimage/__init__.py b/bauh/gems/appimage/__init__.py index 341db84d..7bb34b63 100644 --- a/bauh/gems/appimage/__init__.py +++ b/bauh/gems/appimage/__init__.py @@ -1,7 +1,10 @@ import os from pathlib import Path +from bauh.api.constants import CONFIG_PATH + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) LOCAL_PATH = '{}/.local/share/bauh/appimage'.format(Path.home()) INSTALLATION_PATH = LOCAL_PATH + '/installed/' -SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/suggestions.txt' \ No newline at end of file +SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/suggestions.txt' +CONFIG_FILE = '{}/appimage.yml'.format(CONFIG_PATH) diff --git a/bauh/gems/appimage/config.py b/bauh/gems/appimage/config.py index a19b2ef4..8ceefa84 100644 --- a/bauh/gems/appimage/config.py +++ b/bauh/gems/appimage/config.py @@ -1,5 +1,5 @@ -from bauh.api.constants import CONFIG_PATH from bauh.commons.config import read_config as read +from bauh.gems.appimage import CONFIG_FILE def read_config(update_file: bool = False) -> dict: @@ -9,4 +9,4 @@ def read_config(update_file: bool = False) -> dict: 'enabled': True } } - return read('{}/appimage.yml'.format(CONFIG_PATH), default, update_file=update_file) + return read(CONFIG_FILE, default, update_file=update_file) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 9a4543d0..b1129541 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -7,9 +7,10 @@ import subprocess import traceback from datetime import datetime +from math import floor from pathlib import Path from threading import Lock, Thread -from typing import Set, Type, List +from typing import Set, Type, List, Tuple from colorama import Fore @@ -19,10 +20,12 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority -from bauh.api.abstract.view import MessageType +from bauh.api.abstract.view import MessageType, ViewComponent, FormComponent, InputOption, SingleSelectComponent, \ + SelectViewType, TextInputComponent, PanelComponent +from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess -from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE +from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE, CONFIG_FILE from bauh.gems.appimage.config import read_config from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.worker import DatabaseUpdater @@ -494,5 +497,44 @@ def clear_data(self): os.remove(f) print('{}[bauh][appimage] {} deleted{}'.format(Fore.YELLOW, f, Fore.RESET)) except: - print('{}[bauh][appimage] An exception has happened when deleting {}{}'.format(Fore.RED, Fore.RESET)) + print('{}[bauh][appimage] An exception has happened when deleting {}{}'.format(Fore.RED, f, Fore.RESET)) traceback.print_exc() + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + config = read_config() + max_width = floor(screen_width * 0.15) + + enabled_opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), + InputOption(label=self.i18n['no'].capitalize(), value=False)] + + updater_opts = [ + SingleSelectComponent(label=self.i18n['appimage.config.db_updates.activated'], + options=enabled_opts, + default_option=[o for o in enabled_opts if o.value == config['db_updater']['enabled']][0], + max_per_line=len(enabled_opts), + type_=SelectViewType.RADIO, + tooltip=self.i18n['appimage.config.db_updates.activated.tip'], + max_width=max_width, + id_='up_enabled'), + TextInputComponent(label=self.i18n['interval'], + value=str(config['db_updater']['interval']), + tooltip=self.i18n['appimage.config.db_updates.interval.tip'], + only_int=True, + max_width=max_width, + id_='up_int') + ] + + return PanelComponent([FormComponent(updater_opts, self.i18n['appimage.config.db_updates'])]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + config = read_config() + + panel = component.components[0] + config['db_updater']['enabled'] = panel.get_component('up_enabled').get_selected() + config['db_updater']['interval'] = panel.get_component('up_int').get_int_value() + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca index 072146b7..b8e9429c 100644 --- a/bauh/gems/appimage/resources/locale/ca +++ b/bauh/gems/appimage/resources/locale/ca @@ -17,3 +17,7 @@ appimage.downgrade.uninstall_current_version=No s’ha pogut desinstal·lar la v appimage.downgrade.install_version=No s’ha pogut instal·lar la versió {} ({}) appimage.install.download.error=No s’ha pogut baixar el fitxer {}. El servidor del fitxer pot estar inactiu. appimage.install.appimagelauncher.error = {appimgl} no permet la instal·lació de {app}. Desinstal·leu {appimgl}, reinicieu el sistema i torneu a provar d’instal·lar {app}. +appimage.config.db_updates=Database update +appimage.config.db_updates.activated=activated +appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications +appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de index b7d6df74..11fcbaa6 100644 --- a/bauh/gems/appimage/resources/locale/de +++ b/bauh/gems/appimage/resources/locale/de @@ -16,4 +16,8 @@ appimage.downgrade.first_version={} ist in der ersten veröffentlichten Version appimage.error.uninstall_current_version=Deinstallation der aktuellen Version von {} war nicht möglich appimage.downgrade.install_version=Die Installation der Version {} ({}) war nicht möglich appimage.install.download.error=Das Herunterladen der Datei {} ist fehlgeschlagen. Eventuell ist der Server nicht verfügbar -appimage.install.appimagelauncher.error={appimgl} verhindert die installation von {app}. Deinstalliere {appimgl}, starte deinen Computer neu und versuche die installation von {app} erneut. \ No newline at end of file +appimage.install.appimagelauncher.error={appimgl} verhindert die installation von {app}. Deinstalliere {appimgl}, starte deinen Computer neu und versuche die installation von {app} erneut. +appimage.config.db_updates=Database update +appimage.config.db_updates.activated=activated +appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications +appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index 0a4c5a07..4e887779 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -16,4 +16,8 @@ appimage.downgrade.first_version={} is in its first published version appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} appimage.downgrade.install_version=It was not possible to install the version {} ({}) appimage.install.download.error=It was not possible to download the file {}. The file server can be down. -appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again. \ No newline at end of file +appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again. +appimage.config.db_updates=Database update +appimage.config.db_updates.activated=activated +appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications +appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index 932e3fa1..a51d4dcc 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -16,4 +16,8 @@ appimage.downgrade.first_version={} está en su primera versión publicada appimage.downgrade.uninstall_current_version=No fue posible desinstalar la versión actual de {} appimage.downgrade.install_version=No fue posible instalar la versión {} ({}) appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo. -appimage.install.appimagelauncher.error = {appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente. \ No newline at end of file +appimage.install.appimagelauncher.error={appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente. +appimage.config.db_updates=Actualización de la base de datos +appimage.config.db_updates.activated=activada +appimage.config.db_updates.activated.tip=Será posible buscar actualizaciones relacionadas con las aplicaciones instaladas +appimage.config.db_updates.interval.tip=Intervalo de actualización en SEGUNDOS \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it index 99e32538..a42d6d5a 100644 --- a/bauh/gems/appimage/resources/locale/it +++ b/bauh/gems/appimage/resources/locale/it @@ -17,3 +17,8 @@ appimage.error.uninstall_current_version = Non è stato possibile disinstallare appimage.downgrade.install_version=Non è stato possibile installare la versione {} ({}) appimage.install.download.error=Non è stato possibile scaricare il file {}. Il file server può essere inattivo. appimage.install.appimagelauncher.error={appimgl} non consente l'installazione di {app}. Disinstallare {appimgl}, riavviare il sistema e provare a installare nuovamente {app}. +appimage.config.db_updates=Database update +appimage.config.db_updates.activated=activated +appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications +appimage.config.db_updates.interval.tip=Update interval in SECONDS + diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 43ae6917..376e5cbe 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -16,4 +16,8 @@ appimage.downgrade.first_version={} se encontra em sua primeira versão publicad appimage.downgrade.uninstall_current_version=Não foi possível desinstalar a versão atual de {} appimage.downgrade.install_version=Não foi possivel instalar a versão {} ({}) appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar. -appimage.install.appimagelauncher.error={appimgl} não está permitindo que o {app} seja instalado. Desinstale o {appimgl}, reinicie o sistema e tente instalar o {app} novamente. \ No newline at end of file +appimage.install.appimagelauncher.error={appimgl} não está permitindo que o {app} seja instalado. Desinstale o {appimgl}, reinicie o sistema e tente instalar o {app} novamente. +appimage.config.db_updates=Atualização da base de dados +appimage.config.db_updates.activated=ativada +appimage.config.db_updates.activated.tip=Será possível verificar se há atualizações para os aplicativos instalados +appimage.config.db_updates.interval.tip=Intervalo de atualização em SEGUNDOS \ No newline at end of file diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 5ed024ca..111025f3 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -15,13 +15,37 @@ RE_SRCINFO_KEYS = re.compile(r'(\w+)\s+=\s+(.+)\n') -KNOWN_LIST_FIELDS = ('validpgpkeys', 'depends', 'optdepends', 'sha512sums', 'sha512sums_x86_64', 'source', 'source_x86_64') +KNOWN_LIST_FIELDS = ('validpgpkeys', 'depends', 'optdepends', 'sha512sums', 'sha512sums_x86_64', 'source', 'source_x86_64', 'makedepends') def map_pkgbuild(pkgbuild: str) -> dict: return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)} +def map_srcinfo(string: str, fields: Set[str] = None) -> dict: + info = {} + + if fields: + field_re = re.compile(r'({})\s+=\s+(.+)\n'.format('|'.join(fields))) + else: + field_re = RE_SRCINFO_KEYS + + for match in field_re.finditer(string): + field = match.group(0).split('=') + key = field[0].strip() + val = field[1].strip() + + if key not in info: + info[key] = [val] if key in KNOWN_LIST_FIELDS else val + else: + if not isinstance(info[key], list): + info[key] = [info[key]] + + info[key].append(val) + + return info + + class AURClient: def __init__(self, http_client: HttpClient, logger: logging.Logger): @@ -39,17 +63,21 @@ def get_src_info(self, name: str) -> dict: res = self.http_client.get(URL_SRC_INFO + urllib.parse.quote(name)) if res and res.text: - info = {} - for field in RE_SRCINFO_KEYS.findall(res.text): - if field[0] not in info: - info[field[0]] = [field[1]] if field[0] in KNOWN_LIST_FIELDS else field[1] - else: - if not isinstance(info[field[0]], list): - info[field[0]] = [info[field[0]]] + return map_srcinfo(res.text) + + self.logger.warning('No .SRCINFO found for {}'.format(name)) + self.logger.info('Checking if {} is based on another package'.format(name)) + # if was not found, it may be based on another package. + infos = self.get_info({name}) - info[field[0]].append(field[1]) + if infos: + info = infos[0] - return info + info_name = info.get('Name') + info_base = info.get('PackageBase') + if info_name and info_base and info_name != info_base: + self.logger.info('{p} is based on {b}. Retrieving {b} .SRCINFO'.format(p=info_name, b=info_base)) + return self.get_src_info(info_base) def get_all_dependencies(self, name: str) -> Set[str]: deps = set() diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 0dd25b8d..c701ece8 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -3,5 +3,5 @@ def read_config(update_file: bool = False) -> dict: - template = {'optimize': True, 'transitive_checking': True} + template = {'optimize': True, 'transitive_checking': True, "sync_databases": True} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/confirmation.py b/bauh/gems/arch/confirmation.py index 9c53f7fb..a34e1d44 100644 --- a/bauh/gems/arch/confirmation.py +++ b/bauh/gems/arch/confirmation.py @@ -22,7 +22,7 @@ def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatch view_opts = MultipleSelectComponent(label='', options=opts, - default_options=None) + default_options=set(opts)) install = watcher.request_confirmation(title=i18n['arch.install.optdeps.request.title'], body='

{}.

{}:

'.format(i18n['arch.install.optdeps.request.body'].format(bold(pkgname)), i18n['arch.install.optdeps.request.help']), diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index e2de8b8d..bc9cc51f 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -4,6 +4,9 @@ import shutil import subprocess import time +import traceback +from datetime import datetime +from math import floor from pathlib import Path from threading import Thread from typing import List, Set, Type, Tuple, Dict @@ -15,13 +18,16 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ SuggestionPriority -from bauh.api.abstract.view import MessageType +from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ + ViewComponent, PanelComponent from bauh.commons.category import CategoriesDownloader +from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ SimpleProcess from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, \ - gpg, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE + gpg, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ + CONFIG_FILE from bauh.gems.arch.aur import AURClient from bauh.gems.arch.config import read_config from bauh.gems.arch.depedencies import DependenciesAnalyser @@ -36,7 +42,8 @@ RE_SPLIT_VERSION = re.compile(r'(=|>|<)') SOURCE_FIELDS = ('source', 'source_x86_64') -RE_PRE_DOWNLOADABLE_FILES = re.compile(r'(https?|ftp)://.+\.\w+[^gpg|git]$') +RE_PRE_DOWNLOAD_WL_PROTOCOLS = re.compile(r'^(.+::)?(https?|ftp)://.+') +RE_PRE_DOWNLOAD_BL_EXT = re.compile(r'.+\.(git|gpg)$') SEARCH_OPTIMIZED_MAP = { 'google chrome': 'google-chrome', @@ -187,6 +194,9 @@ def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche handler = ProcessHandler(watcher) app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) + + self._sync_databases(root_password=root_password, handler=handler) + watcher.change_progress(5) try: @@ -195,13 +205,14 @@ def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche if build_dir: watcher.change_progress(10) + base_name = pkg.get_base_name() watcher.change_substatus(self.i18n['arch.clone'].format(bold(pkg.name))) - clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(pkg.name)], cwd=app_build_dir), check_error_output=False)) + clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(base_name)], cwd=app_build_dir), check_error_output=False)) watcher.change_progress(30) if clone: watcher.change_substatus(self.i18n['arch.downgrade.reading_commits']) - clone_path = '{}/{}'.format(app_build_dir, pkg.name) - pkgbuild_path = '{}/PKGBUILD'.format(clone_path) + clone_path = '{}/{}'.format(app_build_dir, base_name) + srcinfo_path = '{}/.SRCINFO'.format(clone_path) commits = run_cmd("git log", cwd=clone_path) watcher.change_progress(40) @@ -210,22 +221,24 @@ def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche commit_list = re.findall(r'commit (.+)\n', commits) if commit_list: if len(commit_list) > 1: + srcfields = {'pkgver', 'pkgrel'} + for idx in range(1, len(commit_list)): commit = commit_list[idx] - with open(pkgbuild_path) as f: - pkgdict = aur.map_pkgbuild(f.read()) + with open(srcinfo_path) as f: + pkgsrc = aur.map_srcinfo(f.read(), srcfields) if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path), check_error_output=False)): watcher.print('Could not downgrade anymore. Aborting...') return False - if '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version: + if '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == pkg.version: # current version found watcher.change_substatus(self.i18n['arch.downgrade.version_found']) break watcher.change_substatus(self.i18n['arch.downgrade.install_older']) - return self._build(pkg.name, pkg.maintainer, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True) + return self._build(pkg.name, base_name, pkg.maintainer, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True) else: watcher.show_message(title=self.i18n['arch.downgrade.error'], body=self.i18n['arch.downgrade.impossible'].format(pkg.name), @@ -346,24 +359,26 @@ def get_history(self, pkg: ArchPackage) -> PackageHistory: try: Path(temp_dir).mkdir(parents=True) - run_cmd('git clone ' + URL_GIT.format(pkg.name), print_error=False, cwd=temp_dir) + base_name = pkg.get_base_name() + run_cmd('git clone ' + URL_GIT.format(base_name), print_error=False, cwd=temp_dir) - clone_path = '{}/{}'.format(temp_dir, pkg.name) - pkgbuild_path = '{}/PKGBUILD'.format(clone_path) + clone_path = '{}/{}'.format(temp_dir, base_name) + srcinfo_path = '{}/.SRCINFO'.format(clone_path) commits = git.list_commits(clone_path) if commits: + srcfields = {'pkgver', 'pkgrel'} history, status_idx = [], -1 for idx, commit in enumerate(commits): - with open(pkgbuild_path) as f: - pkgdict = aur.map_pkgbuild(f.read()) + with open(srcinfo_path) as f: + pkgsrc = aur.map_srcinfo(f.read(), srcfields) - if status_idx < 0 and '{}-{}'.format(pkgdict.get('pkgver'), pkgdict.get('pkgrel')) == pkg.version: + if status_idx < 0 and '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == pkg.version: status_idx = idx - history.append({'1_version': pkgdict['pkgver'], '2_release': pkgdict['pkgrel'], + history.append({'1_version': pkgsrc['pkgver'], '2_release': pkgsrc['pkgrel'], '3_date': commit['date']}) # the number prefix is to ensure the rendering order if idx + 1 < len(commits): @@ -387,9 +402,10 @@ def _install_deps(self, deps: List[Tuple[str, str]], root_password: str, handler self._update_progress(handler.watcher, 1, change_progress) for dep in deps: - handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ()'.format(dep[0], dep[1])))) + handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ({})'.format(dep[0], dep[1])))) if dep[1] == 'aur': - installed = self._install_from_aur(pkgname=dep[0], maintainer=None, root_password=root_password, handler=handler, dependency=True, change_progress=False) + pkgbase = self.aur_client.get_src_info(dep[0])['pkgbase'] + installed = self._install_from_aur(pkgname=dep[0], pkgbase=pkgbase, maintainer=None, root_password=root_password, handler=handler, dependency=True, change_progress=False) else: installed = self._install(pkgname=dep[0], maintainer=None, root_password=root_password, handler=handler, install_file=None, mirror=dep[1], change_progress=False) @@ -412,9 +428,10 @@ def _map_repos(self, pkgnames: Set[str]) -> dict: return pkg_mirrors - def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessWatcher) -> bool: + def _pre_download_source(self, project_dir: str, watcher: ProcessWatcher) -> bool: if self.context.file_downloader.is_multithreaded(): - srcinfo = self.aur_client.get_src_info(pkgname) + with open('{}/.SRCINFO'.format(project_dir)) as f: + srcinfo = aur.map_srcinfo(f.read()) pre_download_files = [] @@ -424,7 +441,7 @@ def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessW continue else: for f in srcinfo[attr]: - if RE_PRE_DOWNLOADABLE_FILES.findall(f): + if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f): pre_download_files.append(f) if pre_download_files: @@ -446,9 +463,9 @@ def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessW def _should_check_subdeps(self): return self.local_config['transitive_checking'] - def _build(self, pkgname: str, maintainer: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: + def _build(self, pkgname: str, base_name: str, maintainer: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: - self._pre_download_source(pkgname, project_dir, handler.watcher) + self._pre_download_source(project_dir, handler.watcher) self._update_progress(handler.watcher, 50, change_progress) @@ -717,7 +734,7 @@ def _import_pgp_keys(self, pkgname: str, root_password: str, handler: ProcessHan handler.watcher.print(self.i18n['action.cancelled']) return False - def _install_from_aur(self, pkgname: str, maintainer: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: + def _install_from_aur(self, pkgname: str, pkgbase: str, maintainer: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) try: @@ -726,20 +743,22 @@ def _install_from_aur(self, pkgname: str, maintainer: str, root_password: str, h self._update_progress(handler.watcher, 10, change_progress) if build_dir: - file_url = URL_PKG_DOWNLOAD.format(pkgname) + base_name = pkgbase if pkgbase else pkgname + file_url = URL_PKG_DOWNLOAD.format(base_name) file_name = file_url.split('/')[-1] handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name))) download = handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=app_build_dir), check_error_output=False)) if download: self._update_progress(handler.watcher, 30, change_progress) - handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(file_name))) - uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(pkgname)], cwd=app_build_dir))) + handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(base_name))) + uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(base_name)], cwd=app_build_dir))) self._update_progress(handler.watcher, 40, change_progress) if uncompress: - uncompress_dir = '{}/{}'.format(app_build_dir, pkgname) + uncompress_dir = '{}/{}'.format(app_build_dir, base_name) return self._build(pkgname=pkgname, + base_name=base_name, maintainer=maintainer, root_password=root_password, handler=handler, @@ -754,6 +773,52 @@ def _install_from_aur(self, pkgname: str, maintainer: str, root_password: str, h return False + def _sync_databases(self, root_password: str, handler: ProcessHandler): + sync_path = '/tmp/bauh/arch/sync' + + if self.local_config['sync_databases']: + if os.path.exists(sync_path): + with open(sync_path) as f: + sync_file = f.read() + + try: + sync_time = datetime.fromtimestamp(int(sync_file)) + now = datetime.now() + + if (now - sync_time).days > 0: + self.logger.info("Package databases synchronization out of date") + else: + msg = "Package databases already synchronized" + self.logger.info(msg) + handler.watcher.print(msg) + return + except: + self.logger.warning("Could not convert the database synchronization time from '{}".format(sync_path)) + traceback.print_exc() + + handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus']) + synced, output = handler.handle_simple(pacman.sync_databases(root_password=root_password, + force=True)) + if synced: + try: + Path('/tmp/bauh/arch').mkdir(parents=True, exist_ok=True) + with open('/tmp/bauh/arch/sync', 'w+') as f: + f.write(str(int(time.time()))) + except: + traceback.print_exc() + else: + self.logger.warning("It was not possible to synchronized the package databases") + handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus.error']) + else: + msg = "Package databases synchronization disabled" + handler.watcher.print(msg) + self.logger.info(msg) + + def _optimize_makepkg(self, watcher: ProcessWatcher): + if self.local_config['optimize'] and not os.path.exists(CUSTOM_MAKEPKG_FILE): + watcher.change_substatus(self.i18n['arch.makepkg.optimizing']) + ArchCompilationOptimizer(self.context.logger).optimize() + def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, skip_optdeps: bool = False) -> bool: clean_config = False @@ -761,11 +826,12 @@ def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, self.local_config = read_config() clean_config = True - if self.local_config['optimize'] and not os.path.exists(CUSTOM_MAKEPKG_FILE): - watcher.change_substatus(self.i18n['arch.makepkg.optimizing']) - ArchCompilationOptimizer(self.context.logger).optimize() + handler = ProcessHandler(watcher) + + self._sync_databases(root_password=root_password, handler=handler) + self._optimize_makepkg(watcher=watcher) - res = self._install_from_aur(pkg.name, pkg.maintainer, root_password, ProcessHandler(watcher), dependency=False, skip_optdeps=skip_optdeps) + res = self._install_from_aur(pkg.name, pkg.package_base, pkg.maintainer, root_password, handler, dependency=False, skip_optdeps=skip_optdeps) if res: if os.path.exists(pkg.get_disk_data_path()): @@ -874,3 +940,54 @@ def launch(self, pkg: ArchPackage): def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass + + def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool, max_width: int) -> SingleSelectComponent: + opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), + InputOption(label=self.i18n['no'].capitalize(), value=False)] + + return SingleSelectComponent(label=self.i18n[label_key].capitalize(), + options=opts, + default_option=[o for o in opts if o.value == value][0], + max_per_line=len(opts), + type_=SelectViewType.RADIO, + tooltip=self.i18n[tooltip_key], + max_width=max_width, + id_=id_) + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + config = read_config() + max_width = floor(screen_width * 0.15) + + fields = [ + self._gen_bool_selector(id_='opts', + label_key='arch.config.optimize', + tooltip_key='arch.config.optimize.tip', + value=config['optimize'], + max_width=max_width), + self._gen_bool_selector(id_='dep_check', + label_key='arch.config.trans_dep_check', + tooltip_key='arch.config.trans_dep_check.tip', + value=config['transitive_checking'], + max_width=max_width), + self._gen_bool_selector(id_='sync_dbs', + label_key='arch.config.sync_dbs', + tooltip_key='arch.config.sync_dbs.tip', + value=config['sync_databases'], + max_width=max_width) + ] + + return PanelComponent([FormComponent(fields, label=self.i18n['installation'].capitalize())]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + config = read_config() + + form_install = component.components[0] + config['optimize'] = form_install.get_component('opts').get_selected() + config['transitive_checking'] = form_install.get_component('dep_check').get_selected() + config['sync_databases'] = form_install.get_component('sync_dbs').get_selected() + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index 85dc4178..5632c885 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -2,13 +2,17 @@ import re from typing import Tuple -from bauh.commons.system import SimpleProcess, ProcessHandler +from bauh.commons.system import SimpleProcess, ProcessHandler, run_cmd from bauh.gems.arch import CUSTOM_MAKEPKG_FILE RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n') RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)') +def gen_srcinfo(build_dir: str) -> str: + return run_cmd('makepkg --printsrcinfo', cwd=build_dir) + + def check(pkgdir: str, optimize: bool, handler: ProcessHandler) -> dict: res = {} diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index 70d02d62..604fbf21 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -66,6 +66,9 @@ def get_type_icon_path(self): def is_application(self): return self.can_be_run() + def get_base_name(self) -> str: + return self.package_base if self.package_base else self.name + def supports_disk_cache(self): return True diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 93dbb75c..a99c7569 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -2,7 +2,7 @@ from threading import Thread from typing import List, Set, Tuple -from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess +from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess from bauh.gems.arch.exceptions import PackageNotFoundException RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]') @@ -324,7 +324,7 @@ def read_provides(name: str) -> Set[str]: if provided_names[0].lower() == 'none': provides = {name} else: - provides = set(provided_names) + provides = {name, *provided_names} return provides @@ -354,3 +354,8 @@ def read_dependencies(name: str) -> Set[str]: depends_on.update([d for d in line.split(' ') if d and d.lower() != 'none']) return depends_on + + +def sync_databases(root_password: str, force: bool = False) -> SimpleProcess: + return SimpleProcess(cmd=['pacman', '-Sy{}'.format('y' if force else '')], + root_password=root_password) diff --git a/bauh/gems/arch/resources/img/arch.svg b/bauh/gems/arch/resources/img/arch.svg index 69267f5c..39445b97 100644 --- a/bauh/gems/arch/resources/img/arch.svg +++ b/bauh/gems/arch/resources/img/arch.svg @@ -1,57 +1 @@ - -image/svg+xml \ No newline at end of file + \ No newline at end of file diff --git a/bauh/gems/arch/resources/img/mirror.svg b/bauh/gems/arch/resources/img/mirror.svg index 74336f2b..246e8690 100644 --- a/bauh/gems/arch/resources/img/mirror.svg +++ b/bauh/gems/arch/resources/img/mirror.svg @@ -8,17 +8,17 @@ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" enable-background="new 0 0 515.91 728.5" - height="24.000002" + height="512" id="Layer_1" version="1.1" - viewBox="0 0 24.000003 24.000002" - width="24.000002" + viewBox="0 0 512.00003 512" + width="512" xml:space="preserve" sodipodi:docname="mirror.svg" inkscape:version="0.92.4 5da689c313, 2019-01-14">image/svg+xml \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 67d1f3de..a5fa5957 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -101,4 +101,12 @@ arch.aur.install.unknown_key.receive_error=No s’ha pogut rebre la clau públic arch.install.aur.unknown_key.body=Per a continuar amb la instal·lació de {} cal confiar en la clau pública següent {} arch.aur.install.validity_check.title=Problemes d’integritat arch.aur.install.validity_check.body=Alguns dels fitxers font necessaris per a la instal·lació de {} són malmesos. La instal·lació es cancel·larà per a evitar danys al vostre sistema. -arch.makepkg.optimizing=Optimitzant la recopilació \ No newline at end of file +arch.makepkg.optimizing=Optimitzant la recopilació +arch.config.optimize=optimize +arch.config.trans_dep_check=check dependencies +arch.config.optimize.tip=Optimized settings will be used in order to make the packages installation faster, otherwise the system settings will be used +arch.config.trans_dep_check.tip=If all the package dependencies should be verified before the installation starts. Otherwise they will be discovered during the installation +arch.sync_databases.substatus=Synchronizing package databases +arch.sync_databases.substatus.error=It was not possible to synchronize the package database +arch.config.sync_dbs=Synchronize packages databases +arch.config.sync_dbs.tip=Synchronizes the package databases once a day ( or after a device reboot ) before the first package installation, upgrade or downgrade. This option help to prevent errors during these operations. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index ac287a6c..be24bbe9 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -101,4 +101,12 @@ aur.info.validpgpkeys=gültige PGP Schlüssel aur.info.options=Optionen aur.info.provides=stellt bereit aur.info.conflicts with=Konflikt mit -arch.makepkg.optimizing=Optimiert die Zusammenstellung \ No newline at end of file +arch.makepkg.optimizing=Optimiert die Zusammenstellung +arch.config.optimize=optimize +arch.config.trans_dep_check=check dependencies +arch.config.optimize.tip=Optimized settings will be used in order to make the packages installation faster, otherwise the system settings will be used +arch.config.trans_dep_check.tip=If all the package dependencies should be verified before the installation starts. Otherwise they will be discovered during the installation +arch.sync_databases.substatus=Synchronizing package databases +arch.sync_databases.substatus.error=It was not possible to synchronize the package database +arch.config.sync_dbs=Synchronize packages databases +arch.config.sync_dbs.tip=Synchronizes the package databases once a day ( or after a device reboot ) before the first package installation, upgrade or downgrade. This option help to prevent errors during these operations. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 8bc34829..3364dbd5 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -101,4 +101,12 @@ aur.info.validpgpkeys=valid PGP keys aur.info.options=options aur.info.provides=provides aur.info.conflicts with=conflicts with -arch.makepkg.optimizing=Optimizing the compilation \ No newline at end of file +arch.makepkg.optimizing=Optimizing the compilation +arch.config.optimize=optimize +arch.config.trans_dep_check=check dependencies +arch.config.optimize.tip=Optimized settings will be used in order to make the packages installation faster, otherwise the system settings will be used +arch.config.trans_dep_check.tip=If all the package dependencies should be verified before the installation starts. Otherwise they will be discovered during the installation +arch.sync_databases.substatus=Synchronizing package databases +arch.sync_databases.substatus.error=It was not possible to synchronize the package database +arch.config.sync_dbs=Synchronize packages databases +arch.config.sync_dbs.tip=Synchronizes the package databases once a day ( or after a device reboot ) before the first package installation, upgrade or downgrade. This option help to prevent errors during these operations. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 49d81205..1724f835 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -101,4 +101,12 @@ arch.aur.install.unknown_key.receive_error=No fue posible recibir la clave públ arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {} arch.aur.install.validity_check.title=Problemas de integridad arch.aur.install.validity_check.body=Algunos de los archivos fuente necesarios para la instalación de {} no están en buen estado. La instalación se cancelará para evitar daños a su sistema. -arch.makepkg.optimizing=Optimizando la compilación \ No newline at end of file +arch.makepkg.optimizing=Optimizando la compilación +arch.config.optimize=optimizar +arch.config.trans_dep_check=verificar dependencias +arch.config.optimize.tip=Se usará una configuración optimizada para acelerar la instalación de los paquetes, de lo contrario se usará la configuración del sistema +arch.config.trans_dep_check.tip=Si todas las dependencias del paquete deben ser verificadas antes de que comience la instalación. De lo contrario, se descubrirán durante la instalación. +arch.sync_databases.substatus=Sincronizando bases de paquetes +arch.sync_databases.substatus.error=No fue posible sincronizar la base de paquetes +arch.config.sync_dbs=Sincronizar las bases de paquetes +arch.config.sync_dbs.tip=Sincroniza las bases de paquetes una vez al día ( o cada reinicialización del dispositivo ) antes de la primera instalación, actualización o reversión de un paquete. Esta opción ayuda a prevenir errores durante estas operaciones. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 302b8bf0..d2ec2cf0 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -69,4 +69,12 @@ arch.aur.install.unknown_key.status=Ricezione della chiave pubblica {} arch.aur.install.unknown_key.receive_error=Impossibile ricevere la chiave pubblica {} arch.aur.install.validity_check.title=Problemi di integrità arch.aur.install.validity_check.body=Alcuni dei file di origine necessari per l'installazione di {} non sono integri. L'installazione verrà annullata per evitare danni al sistema. -arch.makepkg.optimizing=Ottimizzando la compilazione \ No newline at end of file +arch.makepkg.optimizing=Ottimizzando la compilazione +arch.config.optimize=optimize +arch.config.trans_dep_check=check dependencies +arch.config.optimize.tip=Optimized settings will be used in order to make the packages installation faster, otherwise the system settings will be used +arch.config.trans_dep_check.tip=If all the package dependencies should be verified before the installation starts. Otherwise they will be discovered during the installation +arch.sync_databases.substatus=Synchronizing package databases +arch.sync_databases.substatus.error=It was not possible to synchronize the package database +arch.config.sync_dbs=Synchronize packages databases +arch.config.sync_dbs.tip=Synchronizes the package databases once a day ( or after a device reboot ) before the first package installation, upgrade or downgrade. This option help to prevent errors during these operations. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 471ce188..56db3c6e 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -101,4 +101,12 @@ arch.aur.install.unknown_key.status=Recebendo a chave pública {} arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {} arch.aur.install.validity_check.title=Problemas de integridade arch.aur.install.validity_check.body=Alguns dos arquivos-fonte necessários para instalação de {} não estão íntegros. A instalação será cancelada para evitar danos no seu sistema. -arch.makepkg.optimizing=Otimizando a compilação \ No newline at end of file +arch.makepkg.optimizing=Otimizando a compilação +arch.config.optimize=otimizar +arch.config.trans_dep_check=verificar dependências +arch.config.optimize.tip=Utilizará configurações otimizadas para que a instalação de pacotes seja mais rápida, caso contrário utilizará a do sistema +arch.config.trans_dep_check.tip=Se todas as dependências do pacote devem ser verificadas antes da instalação começar, caso contrário elas serão descobertas durante a instalação +arch.sync_databases.substatus=Sincronizando bases de pacotes +arch.sync_databases.substatus.error=Não foi possível sincronizar as bases de pacotes +arch.config.sync_dbs=Sincronizar bases de pacotes +arch.config.sync_dbs.tip=Sincroniza as bases de pacotes uma vez ao dia ( ou a cada reinicialização do dispositivo ) antes da primeira instalação, atualização ou reversão de um pacote. Essa opção ajuda a evitar erros durante essa operações. \ No newline at end of file diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 3ca41c95..24ade860 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -1,6 +1,7 @@ import logging import os import re +import time from multiprocessing import Process from pathlib import Path from threading import Thread @@ -8,6 +9,7 @@ import requests from bauh.api.abstract.context import ApplicationContext +from bauh.commons.system import run_cmd from bauh.gems.arch import pacman, disk, CUSTOM_MAKEPKG_FILE, CONFIG_DIR, BUILD_DIR, \ AUR_INDEX_FILE, config @@ -17,7 +19,6 @@ GLOBAL_MAKEPKG = '/etc/makepkg.conf' RE_MAKE_FLAGS = re.compile(r'#?\s*MAKEFLAGS\s*=\s*.+\s*') -RE_COMPRESS_XZ = re.compile(r'#?\s*COMPRESSXZ\s*=\s*.+') RE_CLEAR_REPLACE = re.compile(r'[\-_.]') @@ -74,8 +75,15 @@ class ArchCompilationOptimizer(Thread): def __init__(self, logger: logging.Logger): super(ArchCompilationOptimizer, self).__init__(daemon=True) self.logger = logger + self.re_compress_xz = re.compile(r'#?\s*COMPRESSXZ\s*=\s*.+') + self.re_build_env = re.compile(r'\s+BUILDENV\s*=.+') + self.re_ccache = re.compile(r'!?ccache') + + def _is_ccache_installed(self) -> bool: + return bool(run_cmd('which ccache', print_error=False)) def optimize(self): + ti = time.time() try: ncpus = os.cpu_count() except: @@ -106,22 +114,50 @@ def optimize(self): else: optimizations.append('MAKEFLAGS="-j$(nproc)"') - compress_xz = RE_COMPRESS_XZ.findall(custom_makepkg if custom_makepkg else global_makepkg) + compress_xz = self.re_compress_xz.findall(custom_makepkg or global_makepkg) if compress_xz: not_eligible = [f for f in compress_xz if not f.startswith('#') and '--threads' in f] if not not_eligible: - custom_makepkg = RE_COMPRESS_XZ.sub('', global_makepkg) + custom_makepkg = self.re_compress_xz.sub('', custom_makepkg or global_makepkg) optimizations.append('COMPRESSXZ=(xz -c -z - --threads=0)') else: self.logger.warning("It seems '{}' COMPRESSXZ is already customized".format(GLOBAL_MAKEPKG)) else: optimizations.append('COMPRESSXZ=(xz -c -z - --threads=0)') + build_envs = self.re_build_env.findall(custom_makepkg or global_makepkg) + + if build_envs: + build_def = None + for e in build_envs: + env_line = e.strip() + + ccache_defs = self.re_ccache.findall(env_line) + ccache_installed = self._is_ccache_installed() + + if ccache_defs: + if ccache_installed: + custom_makepkg = (custom_makepkg or global_makepkg).replace(e, '') + + if not build_def: + build_def = self.re_ccache.sub('', env_line).replace('(', '(ccache ') + elif not build_def: + build_def = self.re_ccache.sub('', env_line) + + if build_def: + optimizations.append(build_def) + else: + self.logger.warning("No BUILDENV declaration found") + + if self._is_ccache_installed(): + self.logger.info('Adding a BUILDENV declaration') + optimizations.append('BUILDENV=(ccache)') + if optimizations: generated_by = '# \n' - custom_makepkg = generated_by + custom_makepkg + '\n' + generated_by + '\n'.join(optimizations) + '\n' + custom_makepkg = custom_makepkg + '\n' + generated_by + '\n'.join(optimizations) + '\n' with open(CUSTOM_MAKEPKG_FILE, 'w+') as f: f.write(custom_makepkg) @@ -134,6 +170,8 @@ def optimize(self): self.logger.info("Removing old optimized 'makepkg.conf' at '{}'".format(CUSTOM_MAKEPKG_FILE)) os.remove(CUSTOM_MAKEPKG_FILE) + tf = time.time() + self.logger.info("Optimizations took {0:.2f} seconds".format(tf - ti)) self.logger.info('Finished') def run(self): diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index f97fe971..c1fee9dc 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -1,15 +1,18 @@ import traceback from datetime import datetime +from math import floor from threading import Thread -from typing import List, Set, Type +from typing import List, Set, Type, Tuple from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext from bauh.api.abstract.disk import DiskCacheLoader 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.api.abstract.view import MessageType, FormComponent, SingleSelectComponent, InputOption, SelectViewType, \ + ViewComponent, PanelComponent from bauh.commons import user +from bauh.commons.config import save_config from bauh.commons.html import strip_html, bold from bauh.commons.system import SystemProcess, ProcessHandler, SimpleProcess from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE @@ -151,11 +154,15 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app return SearchResult(models, None, len(models)) def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: + handler = ProcessHandler(watcher) 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, pkg.installation) + commits = flatpak.get_app_commits(pkg.ref, pkg.origin, pkg.installation, handler) + + if commits is None: + return False commit_idx = commits.index(pkg.commit) @@ -167,9 +174,9 @@ def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: Proces 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, pkg.installation, root_password), - success_phrases=['Changes complete.', 'Updates complete.'], - wrong_error_phrase='Warning')) + success = handler.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) return success @@ -178,7 +185,17 @@ def clean_cache_for(self, pkg: FlatpakApplication): 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, pkg.installation))) + related, deps = False, False + ref = pkg.ref + + if pkg.partial and flatpak.get_version() < '1.5': + related, deps = True, True + ref = pkg.base_ref + + return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(app_ref=ref, + installation=pkg.installation, + related=related, + deps=deps))) def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref, pkg.installation))) @@ -190,7 +207,14 @@ def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: Proces def get_info(self, app: FlatpakApplication) -> dict: if app.installed: - app_info = flatpak.get_app_info_fields(app.id, app.branch, app.installation) + version = flatpak.get_version() + id_ = app.base_id if app.partial and version < '1.5' else app.id + app_info = flatpak.get_app_info_fields(id_, app.branch, app.installation) + + if app.partial and version < '1.5': + app_info['id'] = app.id + app_info['ref'] = app.ref + 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 '' @@ -314,7 +338,7 @@ def requires_root(self, action: str, pkg: FlatpakApplication): return action == 'downgrade' and pkg.installation == 'system' def prepare(self): - pass + Thread(target=read_config, daemon=True).start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: updates = [] @@ -411,3 +435,37 @@ def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: traceback.print_exc() return urls + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + fields = [] + + config = read_config() + + install_opts = [InputOption(label=self.i18n['flatpak.config.install_level.system'].capitalize(), + value='system', + tooltip=self.i18n['flatpak.config.install_level.system.tip']), + InputOption(label=self.i18n['flatpak.config.install_level.user'].capitalize(), + value='user', + tooltip=self.i18n['flatpak.config.install_level.user.tip']), + InputOption(label=self.i18n['flatpak.config.install_level.ask'].capitalize(), + value=None, + tooltip=self.i18n['flatpak.config.install_level.ask.tip'].format(app=self.context.app_name))] + fields.append(SingleSelectComponent(label=self.i18n['flatpak.config.install_level'], + options=install_opts, + default_option=[o for o in install_opts if o.value == config['installation_level']][0], + max_per_line=len(install_opts), + max_width=floor(screen_width * 0.22), + type_=SelectViewType.RADIO)) + + return PanelComponent([FormComponent(fields, self.i18n['installation'].capitalize())]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + config = read_config() + config['installation_level'] = component.components[0].components[0].get_selected() + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] + diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index 471898de..c9401f7a 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -5,14 +5,18 @@ from typing import List, Dict, Set from bauh.api.exception import NoInternetException -from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess +from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess, ProcessHandler -BASE_CMD = 'flatpak' RE_SEVERAL_SPACES = re.compile(r'\s+') 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)) + info = get_app_info(app_id, branch, installation) + + if not info: + return {} + + info = re.findall(r'\w+:\s.+', info) data = {} fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0) @@ -37,7 +41,7 @@ def get_app_info_fields(app_id: str, branch: str, installation: str, fields: Lis def get_fields(app_id: str, branch: str, fields: List[str]) -> List[str]: - cmd = [BASE_CMD, 'info', app_id] + cmd = ['flatpak', 'info', app_id] if branch: cmd.append(branch) @@ -58,16 +62,20 @@ def is_installed(): def get_version(): - res = run_cmd('{} --version'.format(BASE_CMD), print_error=False) + res = run_cmd('{} --version'.format('flatpak'), print_error=False) return res.split(' ')[1].strip() if res else None def get_app_info(app_id: str, branch: str, installation: str): - return run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch, '--{}'.format(installation))) + try: + return run_cmd('{} info {} {}'.format('flatpak', app_id, branch, '--{}'.format(installation))) + except: + traceback.print_exc() + return '' def get_commit(app_id: str, branch: str, installation: str) -> str: - info = new_subprocess([BASE_CMD, 'info', app_id, branch, '--{}'.format(installation)]) + info = new_subprocess(['flatpak', 'info', app_id, branch, '--{}'.format(installation)]) for o in new_subprocess(['grep', 'Commit:', '--color=never'], stdin=info.stdout).stdout: if o: @@ -79,7 +87,7 @@ def list_installed(version: str) -> List[dict]: apps = [] if version < '1.2': - app_list = new_subprocess([BASE_CMD, 'list', '-d']) + app_list = new_subprocess(['flatpak', 'list', '-d']) for o in app_list.stdout: if o: @@ -102,7 +110,7 @@ def list_installed(version: str) -> List[dict]: else: cols = 'application,ref,arch,branch,description,origin,options,{}version'.format('' if version < '1.3' else 'name,') - app_list = new_subprocess([BASE_CMD, 'list', '--columns=' + cols]) + app_list = new_subprocess(['flatpak', 'list', '--columns=' + cols]) for o in app_list.stdout: if o: @@ -141,22 +149,21 @@ def list_installed(version: str) -> List[dict]: return apps -def update(app_ref: str, installation: str): +def update(app_ref: str, installation: str, related: bool = False, deps: bool = False): """ Updates the app reference :param app_ref: :return: """ - return new_subprocess([BASE_CMD, 'update', '--no-related', '--no-deps', '-y', app_ref, '--{}'.format(installation)]) + cmd = ['flatpak', 'update', '-y', app_ref, '--{}'.format(installation)] + + if not related: + cmd.append('--no-related') + if not deps: + cmd.append('--no-deps') -def register_flathub(installation: str) -> SimpleProcess: - return SimpleProcess([BASE_CMD, - 'remote-add', - '--if-not-exists', - 'flathub', - 'https://flathub.org/repo/flathub.flatpakrepo', - '--{}'.format(installation)]) + return new_subprocess(cmd) def uninstall(app_ref: str, installation: str): @@ -165,7 +172,7 @@ def uninstall(app_ref: str, installation: str): :param app_ref: :return: """ - return new_subprocess([BASE_CMD, 'uninstall', app_ref, '-y', '--{}'.format(installation)]) + return new_subprocess(['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)]) def list_updates_as_str(version: str) -> Dict[str, set]: @@ -182,7 +189,7 @@ def read_updates(version: str, installation: str) -> Dict[str, set]: res = {'partial': set(), 'full': set()} if version < '1.2': try: - output = run_cmd('{} update --no-related --no-deps --{}'.format(BASE_CMD, installation), ignore_return_code=True) + output = run_cmd('{} update --no-related --no-deps --{}'.format('flatpak', 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'): @@ -191,7 +198,7 @@ def read_updates(version: str, installation: str) -> Dict[str, set]: except: traceback.print_exc() else: - updates = new_subprocess([BASE_CMD, 'update', '--{}'.format(installation)]).stdout + updates = new_subprocess(['flatpak', 'update', '--{}'.format(installation)]).stdout reg = r'[0-9]+\.\s+.+' @@ -220,7 +227,7 @@ def read_updates(version: str, installation: str) -> Dict[str, set]: 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)] + cmd = ['flatpak', 'update', '--no-related', '--no-deps', '--commit={}'.format(commit), app_ref, '-y', '--{}'.format(installation)] if installation == 'system': return new_root_subprocess(cmd, root_password) @@ -228,17 +235,20 @@ def downgrade(app_ref: str, commit: str, installation: str, root_password: str) return new_subprocess(cmd) -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) - else: +def get_app_commits(app_ref: str, origin: str, installation: str, handler: ProcessHandler) -> List[str]: + try: + p = SimpleProcess(['flatpak', 'remote-info', '--log', origin, app_ref, '--{}'.format(installation)]) + success, output = handler.handle_simple(p) + if output.startswith('error:'): + return + else: + return re.findall(r'Commit+:\s(.+)', output) + except: raise NoInternetException() 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)) + log = run_cmd('{} remote-info --log {} {} --{}'.format('flatpak', origin, app_ref, installation)) if not log: raise NoInternetException() @@ -265,7 +275,7 @@ def get_app_commits_data(app_ref: str, origin: str, installation: str) -> List[d def search(version: str, word: str, installation: str, app_id: bool = False) -> List[dict]: - res = run_cmd('{} search {} --{}'.format(BASE_CMD, word, installation)) + res = run_cmd('{} search {} --{}'.format('flatpak', word, installation)) found = [] @@ -343,21 +353,21 @@ def search(version: str, word: str, installation: str, app_id: bool = False) -> def install(app_id: str, origin: str, installation: str): - return new_subprocess([BASE_CMD, 'install', origin, app_id, '-y', '--{}'.format(installation)]) + return new_subprocess(['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)]) 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)] + cmd = ['flatpak', '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()) + return bool(run_cmd('{} remotes'.format('flatpak')).strip()) def list_remotes() -> Dict[str, Set[str]]: res = {'system': set(), 'user': set()} - output = run_cmd('{} remotes'.format(BASE_CMD)).strip() + output = run_cmd('{} remotes'.format('flatpak')).strip() if output: lines = output.split('\n') @@ -374,4 +384,4 @@ def list_remotes() -> Dict[str, Set[str]]: def run(app_id: str): - subprocess.Popen([BASE_CMD, 'run', app_id]) + subprocess.Popen(['flatpak', 'run', app_id]) diff --git a/bauh/gems/flatpak/model.py b/bauh/gems/flatpak/model.py index 880f54e0..ae70d4ec 100644 --- a/bauh/gems/flatpak/model.py +++ b/bauh/gems/flatpak/model.py @@ -22,6 +22,8 @@ def __init__(self, id: str = None, name: str = None, version: str = None, latest self.partial = False self.installation = installation if installation else 'system' self.i18n = i18n + self.base_id = None + self.base_ref = None if runtime: self.categories = ['runtime'] @@ -77,8 +79,10 @@ def get_publisher(self): def gen_partial(self, partial_id: str) -> "FlatpakApplication": partial = copy.deepcopy(self) partial.id = partial_id + partial.base_id = self.id if self.ref: + partial.base_ref = self.ref partial.ref = '/'.join((partial_id, *self.ref.split('/')[1:])) partial.partial = True diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index 10f50234..3a61c697 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -1,4 +1,4 @@ -gem.flatpak.info=Aplicacions disponibles als dipòsits configurats en el vostre sistema +gem.flatpak.info=Aplicacions disponibles a Flathub i altres repositoris configurats al vostre sistema flatpak.info.arch=arquitectura flatpak.info.branch=branca flatpak.info.collection=col·lecció @@ -45,4 +45,11 @@ 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 +flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote ) +flatpak.config.install_level.system=system +flatpak.config.install_level.system.tip=Applications will be installed for all the device users +flatpak.config.install_level.user=user +flatpak.config.install_level.user.tip=Application will be installed only for the current user +flatpak.config.install_level.ask=ask +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation +flatpak.config.install_level=level \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index dc60e06b..707a44c0 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -44,4 +44,11 @@ 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 +flatpak.remotes.system_flathub.error=Flathub konnte nicht als System-Repository ( remote ) hinzugefügt werden +flatpak.config.install_level.system=system +flatpak.config.install_level.system.tip=Applications will be installed for all the device users +flatpak.config.install_level.user=user +flatpak.config.install_level.user.tip=Application will be installed only for the current user +flatpak.config.install_level.ask=ask +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation +flatpak.config.install_level=level \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index 4176e3cf..fb571bbd 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -1,4 +1,4 @@ -gem.flatpak.info=Applications available in the repositories configured on your system +gem.flatpak.info=Applications available on Flathub and other repositories configured on your system flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} flatpak.downgrade.impossible.title=Error @@ -44,4 +44,11 @@ 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 +flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote ) +flatpak.config.install_level.system=system +flatpak.config.install_level.system.tip=Applications will be installed for all the device users +flatpak.config.install_level.user=user +flatpak.config.install_level.user.tip=Application will be installed only for the current user +flatpak.config.install_level.ask=ask +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation +flatpak.config.install_level=level \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index 2aaa0e92..fd6cfce9 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -1,4 +1,4 @@ -gem.flatpak.info=Aplicativos disponibles en los repositorios configurados en su sistema +gem.flatpak.info=Aplicaciones disponibles en Flathub y otros repositorios configurados en su sistema flatpak.info.arch=arquitectura flatpak.info.branch=rama flatpak.info.collection=colección @@ -45,4 +45,11 @@ 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 +flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote ) +flatpak.config.install_level.system=sistema +flatpak.config.install_level.system.tip=Se instalarán aplicaciones para todos los usuarios del dispositivo +flatpak.config.install_level.user=usuario +flatpak.config.install_level.user.tip=La aplicación se instalará solo para el usuario actual +flatpak.config.install_level.ask=preguntar +flatpak.config.install_level.ask.tip={app} preguntará el nivel que debe aplicarse durante la instalación de la aplicación +flatpak.config.install_level=nivel \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index 42521925..c0351a93 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -1,4 +1,4 @@ -gem.flatpak.info=Applicazioni disponibili nei repository configurati sul sistema +gem.flatpak.info=Applicazioni disponibili su Flathub e altri repository configurati sul tuo sistema flatpak.notification.no_remotes=Nessun set remoti Flatpak. Non sarà possibile cercare app Flatpak. flatpak.notification.disable=Ise non si desidera utilizzare le applicazioni Flatpak, deselezionare {} in {} flatpak.downgrade.impossible.title=Errore @@ -24,4 +24,11 @@ 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 +flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote ) +flatpak.config.install_level.system=system +flatpak.config.install_level.system.tip=Applications will be installed for all the device users +flatpak.config.install_level.user=user +flatpak.config.install_level.user.tip=Application will be installed only for the current user +flatpak.config.install_level.ask=ask +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation +flatpak.config.install_level=level \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index bd1a5301..1d0b3e82 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -1,4 +1,4 @@ -gem.flatpak.info=Aplicativos disponíveis nos repositórios configurados do seu sistema +gem.flatpak.info=Aplicativos disponíveis no Flathub e outros repositórios configurados no seu sistema flatpak.info.arch=arquitetura flatpak.info.branch=ramo flatpak.info.collection=coleção @@ -45,4 +45,11 @@ 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 +flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote ) +flatpak.config.install_level.system=sistema +flatpak.config.install_level.system.tip=Os aplicativos serão instalados para todos os usuários do dispositivo +flatpak.config.install_level.user=usuário +flatpak.config.install_level.user.tip=Os aplicativos serão instalados somente para o usuário atual +flatpak.config.install_level.ask=perguntar +flatpak.config.install_level.ask.tip=O {app} perguntará qual o nível deverá ser aplicado durante a instalação do aplicativo +flatpak.config.install_level=nível \ No newline at end of file diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 183b2ab0..bcf2cef2 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -217,7 +217,8 @@ def list_warnings(self, internet_available: bool) -> List[str]: if not snap.is_snapd_running(): snap_bold = bold('Snap') return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), - self.i18n['snap.notification.snap.disable'].format(snap_bold, bold(self.i18n['manage_window.settings.gems']))] + self.i18n['snap.notification.snap.disable'].format(snap_bold, bold('{} > {}'.format(self.i18n['settings'].capitalize(), + self.i18n['core.config.tab.types'])))] elif internet_available: available, output = snap.is_api_available() diff --git a/bauh/gems/web/config.py b/bauh/gems/web/config.py index b532704a..6c8e402b 100644 --- a/bauh/gems/web/config.py +++ b/bauh/gems/web/config.py @@ -12,3 +12,4 @@ def read_config(update_file: bool = False) -> dict: return read(CONFIG_FILE, default_config, update_file) + diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index d58db812..a8eaef11 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -5,6 +5,7 @@ import shutil import subprocess import traceback +from math import floor from pathlib import Path from threading import Thread from typing import List, Type, Set, Tuple @@ -21,13 +22,14 @@ from bauh.api.abstract.model import SoftwarePackage, PackageAction, PackageSuggestion, PackageUpdate, PackageHistory, \ SuggestionPriority, PackageStatus from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, SingleSelectComponent, \ - SelectViewType, TextInputComponent, FormComponent, FileChooserComponent + SelectViewType, TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, PanelComponent from bauh.api.constants import DESKTOP_ENTRIES_DIR from bauh.commons import resource +from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import ProcessHandler, get_dir_size, get_human_size_str from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN, URL_FIX_PATTERN, ENV_PATH, UA_CHROME, \ - SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR + SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR, CONFIG_FILE from bauh.gems.web.config import read_config from bauh.gems.web.environment import EnvironmentUpdater, EnvironmentComponent from bauh.gems.web.model import WebApplication @@ -889,3 +891,54 @@ def clear_data(self): except: print('{}[bauh][web] An exception has happened when deleting {}{}'.format(Fore.RED, ENV_PATH, Fore.RESET)) traceback.print_exc() + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + config = read_config() + max_width = floor(screen_width * 0.15) + + input_electron = TextInputComponent(label=self.i18n['web.settings.electron.version.label'], + value=config['environment']['electron']['version'], + tooltip=self.i18n['web.settings.electron.version.tooltip'], + placeholder='{}: 7.1.0'.format(self.i18n['example.short']), + max_width=max_width, + id_='electron_version') + + native_opts = [ + InputOption(label=self.i18n['web.settings.nativefier.env'].capitalize(), value=False, tooltip=self.i18n['web.settings.nativefier.env.tooltip'].format(app=self.context.app_name)), + InputOption(label=self.i18n['web.settings.nativefier.system'].capitalize(), value=True, tooltip=self.i18n['web.settings.nativefier.system.tooltip']) + ] + + select_nativefier = SingleSelectComponent(label="Nativefier", + options=native_opts, + default_option=[o for o in native_opts if o.value == config['environment']['system']][0], + type_=SelectViewType.COMBO, + tooltip=self.i18n['web.settings.nativefier.tip'], + max_width=max_width, + id_='nativefier') + + form_env = FormComponent(label=self.i18n['web.settings.nativefier.env'].capitalize(), components=[input_electron, select_nativefier]) + + return PanelComponent([form_env]) + + def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + config = read_config() + + form_env = component.components[0] + + config['environment']['electron']['version'] = str(form_env.get_component('electron_version').get_value()).strip() + + if len(config['environment']['electron']['version']) == 0: + config['environment']['electron']['version'] = None + + system_nativefier = form_env.get_component('nativefier').get_selected() + + if system_nativefier and not nativefier.is_available(): + return False, [self.i18n['web.settings.env.nativefier.system.not_installed'].format('Nativefier')] + + config['environment']['system'] = system_nativefier + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en index a5c71ee8..136433da 100644 --- a/bauh/gems/web/resources/locale/en +++ b/bauh/gems/web/resources/locale/en @@ -58,4 +58,12 @@ web.info.06_desktop_entry=shortcut web.info.07_exec_file=executable web.info.08_icon_path=icon web.info.09_size=size -web.info.10_config_dir=configuration dir \ No newline at end of file +web.info.10_config_dir=configuration dir +web.settings.electron.version.label=Electron version +web.settings.electron.version.tooltip=Defines an alternative Electron version to render the new installed apps +web.settings.nativefier.env=environment +web.settings.nativefier.system=system +web.settings.nativefier.env.tooltip=The Nativefier version installed on the isolated {app} environment will be used to install applications +web.settings.nativefier.system.tooltip=The Nativefier version installed on your system will be used to install applications +web.settings.nativefier.tip=Defines which Nativefier version should be used to generate the Web applications +web.settings.env.nativefier.system.not_installed={} seems not to be installed on your system \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/es b/bauh/gems/web/resources/locale/es index 7030c2f1..e38065ea 100644 --- a/bauh/gems/web/resources/locale/es +++ b/bauh/gems/web/resources/locale/es @@ -58,4 +58,12 @@ web.info.06_desktop_entry=atajo web.info.07_exec_file=ejecutable web.info.08_icon_path=icono web.info.09_size=tamaño -web.info.10_config_dir=directorio de configuración \ No newline at end of file +web.info.10_config_dir=directorio de configuración +web.settings.electron.version.label=Versión del Electron +web.settings.electron.version.tooltip=Define una versión alternativa del Electron para renderizar las nuevas aplicaciones instaladas +web.settings.nativefier.env=ambiente +web.settings.nativefier.system=sistema +web.settings.nativefier.env.tooltip=Se utilizará la versión de Nativefier instalada en el ambiente aislado de {app} para instalar aplicaciones +web.settings.nativefier.system.tooltip=Se utilizará la versión de Nativefier instalada en su sistema para instalar aplicaciones +web.settings.nativefier.tip=Define qué versión de Nativefier debe usarse para generar las aplicaciones Web +web.settings.env.nativefier.system.not_installed={} parece no estar instalado en su sistema \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt index fc9ce569..8e5de011 100644 --- a/bauh/gems/web/resources/locale/pt +++ b/bauh/gems/web/resources/locale/pt @@ -58,4 +58,12 @@ web.info.06_desktop_entry=atalho web.info.07_exec_file=executável web.info.08_icon_path=ícone web.info.09_size=tamanho -web.info.10_config_dir=diretório de configuração \ No newline at end of file +web.info.10_config_dir=diretório de configuração +web.settings.electron.version.label=Versão do Electron +web.settings.electron.version.tooltip=Define uma versão alternativa do Electron para renderizar os novos aplicativos instalados +web.settings.nativefier.env=ambiente +web.settings.nativefier.system=sistema +web.settings.nativefier.env.tooltip=A versão do Nativefier instalada no ambiente isolado do {app} será utilizada para instalar aplicativos +web.settings.nativefier.system.tooltip=A versão do Nativefier instalada no seu sistema será utilizada para instalar aplicativos +web.settings.nativefier.tip=Define qual versão do Nativefier será utilizada para gerar os aplicativos Web +web.settings.env.nativefier.system.not_installed={} não parece estar instalado no seu sistema \ No newline at end of file diff --git a/bauh/view/core/config.py b/bauh/view/core/config.py index df00a272..c8c1e8e1 100644 --- a/bauh/view/core/config.py +++ b/bauh/view/core/config.py @@ -43,7 +43,10 @@ def read_config(update_file: bool = False) -> dict: 'default_icon': None, 'updates_icon': None }, - 'style': None + 'style': None, + 'hdpi': True, + "auto_scale": False + }, 'download': { 'multithreaded': True, diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 9f2f4075..c6bd6f6f 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -2,21 +2,24 @@ import time import traceback from threading import Thread -from typing import List, Set, Type +from typing import List, Set, Type, Tuple from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction +from bauh.api.abstract.view import ViewComponent, TabGroupComponent from bauh.api.exception import NoInternetException from bauh.commons import internet +from bauh.view.core.settings import GenericSettingsManager RE_IS_URL = re.compile(r'^https?://.+') class GenericSoftwareManager(SoftwareManager): - def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict): + def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict, + settings_manager: GenericSettingsManager = None): super(GenericSoftwareManager, self).__init__(context=context) self.managers = managers self.map = {t: m for m in self.managers for t in m.get_managed_types()} @@ -28,6 +31,7 @@ def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, self._already_prepared = [] self.working_managers = [] self.config = config + self.settings_manager = settings_manager def reset_cache(self): if self._available_cache is not None: @@ -211,7 +215,11 @@ def downgrade(self, app: SoftwarePackage, root_password: str, handler: ProcessWa man = self._get_manager_for(app) if man and app.can_be_downgraded(): - return man.downgrade(app, root_password, handler) + mti = time.time() + res = man.downgrade(app, root_password, handler) + mtf = time.time() + self.logger.info('Took {0:.2f} seconds'.format(mtf - mti)) + return res else: raise Exception("downgrade is not possible for {}".format(app.__class__.__name__)) @@ -258,7 +266,11 @@ def get_history(self, app: SoftwarePackage) -> PackageHistory: man = self._get_manager_for(app) if man: - return man.get_history(app) + mti = time.time() + history = man.get_history(app) + mtf = time.time() + self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti)) + return history def get_managed_types(self) -> Set[Type[SoftwarePackage]]: pass @@ -390,3 +402,20 @@ def get_screenshots(self, pkg: SoftwarePackage): def get_working_managers(self): return [m for m in self.managers if self._can_work(m)] + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + if self.settings_manager is None: + self.settings_manager = GenericSettingsManager(managers=self.managers, + working_managers=self.working_managers, + logger=self.logger, + i18n=self.i18n) + + return self.settings_manager.get_settings(screen_width=screen_width, screen_height=screen_height) + + def save_settings(self, component: TabGroupComponent) -> Tuple[bool, List[str]]: + res = self.settings_manager.save_settings(component) + + if res[0]: + self.settings_manager = None + + return res diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py index 2b5f0a48..34d2f220 100644 --- a/bauh/view/core/downloader.py +++ b/bauh/view/core/downloader.py @@ -1,5 +1,6 @@ import logging import os +import re import time import traceback @@ -10,6 +11,7 @@ from bauh.commons.system import run_cmd, new_subprocess, ProcessHandler, SystemProcess, SimpleProcess from bauh.view.util.translation import I18n +RE_HAS_EXTENSION = re.compile(r'.+\.\w+$') class AdaptableFileDownloader(FileDownloader): @@ -90,7 +92,13 @@ def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = No downloader = 'wget' file_size = self.http_client.get_content_length(file_url) - msg = bold('[{}] ').format(downloader) + self.i18n['downloading'] + ' ' + bold(file_url.split('/')[-1]) + (' ( {} )'.format(file_size) if file_size else '') + + name = file_url.split('/')[-1] + + if output_path and not RE_HAS_EXTENSION.match(name) and RE_HAS_EXTENSION.match(output_path): + name = output_path.split('/')[-1] + + msg = bold('[{}] ').format(downloader) + self.i18n['downloading'] + ' ' + bold(name) + (' ( {} )'.format(file_size) if file_size else '') if watcher: watcher.change_substatus(msg) diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py new file mode 100644 index 00000000..97ab51c5 --- /dev/null +++ b/bauh/view/core/settings.py @@ -0,0 +1,359 @@ +import logging +import os +import traceback +from math import floor +from typing import List, Tuple + +from PyQt5.QtWidgets import QApplication, QStyleFactory + +from bauh import ROOT_DIR +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.abstract.view import ViewComponent, TabComponent, InputOption, TextComponent, MultipleSelectComponent, \ + PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \ + FileChooserComponent +from bauh.view.core import config +from bauh.view.core.config import read_config +from bauh.view.util import translation +from bauh.view.util.translation import I18n + + +class GenericSettingsManager: + + def __init__(self, managers: List[SoftwareManager], working_managers: List[SoftwareManager], + logger: logging.Logger, i18n: I18n): + self.i18n = i18n + self.managers = managers + self.working_managers = working_managers + self.logger = logger + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + tabs = list() + + gem_opts, def_gem_opts, gem_tabs = [], set(), [] + + for man in self.managers: + if man.can_work(): + man_comp = man.get_settings(screen_width, screen_height) + modname = man.__module__.split('.')[-2] + icon_path = "{r}/gems/{n}/resources/img/{n}.svg".format(r=ROOT_DIR, n=modname) + + if man_comp: + gem_tabs.append(TabComponent(label=modname.capitalize(), content=man_comp, icon_path=icon_path, id_=modname)) + + opt = InputOption(label=self.i18n.get('gem.{}.label'.format(modname), modname.capitalize()), + tooltip=self.i18n.get('gem.{}.info'.format(modname)), + value=modname, + icon_path='{r}/gems/{n}/resources/img/{n}.svg'.format(r=ROOT_DIR, n=modname)) + gem_opts.append(opt) + + if man.is_enabled() and man in self.working_managers: + def_gem_opts.add(opt) + + core_config = read_config() + + if gem_opts: + type_help = TextComponent(html=self.i18n['core.config.types.tip']) + gem_opts.sort(key=lambda o: o.value) + gem_selector = MultipleSelectComponent(label=None, + tooltip=None, + options=gem_opts, + max_width=floor(screen_width * 0.22), + default_options=def_gem_opts, + id_="gems") + tabs.append(TabComponent(label=self.i18n['core.config.tab.types'], + content=PanelComponent([type_help, FormComponent([gem_selector], spaces=False)]), + id_='core.types')) + + tabs.append(self._gen_general_settings(core_config, screen_width, screen_height)) + tabs.append(self._gen_ui_settings(core_config, screen_width, screen_height)) + tabs.append(self._gen_tray_settings(core_config, screen_width, screen_height)) + tabs.append(self._gen_adv_settings(core_config, screen_width, screen_height)) + + for tab in gem_tabs: + tabs.append(tab) + + return TabGroupComponent(tabs) + + def _gen_adv_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: + default_width = floor(0.11 * screen_width) + + select_dcache = self._gen_bool_component(label=self.i18n['core.config.disk_cache'], + tooltip=self.i18n['core.config.disk_cache.tip'], + value=core_config['disk_cache']['enabled'], + id_='dcache') + + input_data_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.data_exp'], + tooltip=self.i18n['core.config.mem_cache.data_exp.tip'], + value=str(core_config['memory_cache']['data_expiration']), + only_int=True, + max_width=default_width, + id_="data_exp") + + input_icon_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.icon_exp'], + tooltip=self.i18n['core.config.mem_cache.icon_exp.tip'], + value=str(core_config['memory_cache']['icon_expiration']), + only_int=True, + max_width=default_width, + id_="icon_exp") + + select_dep_check = self._gen_bool_component(label=self.i18n['core.config.system.dep_checking'], + tooltip=self.i18n['core.config.system.dep_checking.tip'], + value=core_config['system']['single_dependency_checking'], + max_width=default_width, + id_='dep_check') + + select_dmthread = self._gen_bool_component(label=self.i18n['core.config.download.multithreaded'], + tooltip=self.i18n['core.config.download.multithreaded.tip'], + id_="down_mthread", + max_width=default_width, + value=core_config['download']['multithreaded']) + + sub_comps = [FormComponent([select_dcache, select_dmthread, select_dep_check, input_data_exp, input_icon_exp], spaces=False)] + return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), PanelComponent(sub_comps), None, 'core.adv') + + def _gen_tray_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: + default_width = floor(0.22 * screen_width) + + input_update_interval = TextInputComponent(label=self.i18n['core.config.updates.interval'].capitalize(), + tooltip=self.i18n['core.config.updates.interval.tip'], + only_int=True, + value=str(core_config['updates']['check_interval']), + max_width=default_width, + id_="updates_interval") + + allowed_exts = {'png', 'svg', 'jpg', 'jpeg', 'ico', 'xpm'} + select_def_icon = FileChooserComponent(id_='def_icon', + label=self.i18n["core.config.ui.tray.default_icon"].capitalize(), + tooltip=self.i18n["core.config.ui.tray.default_icon.tip"].capitalize(), + file_path=str(core_config['ui']['tray']['default_icon']) if core_config['ui']['tray']['default_icon'] else None, + max_width=default_width, + allowed_extensions=allowed_exts) + + select_up_icon = FileChooserComponent(id_='up_icon', + label=self.i18n["core.config.ui.tray.updates_icon"].capitalize(), + tooltip=self.i18n["core.config.ui.tray.updates_icon.tip"].capitalize(), + file_path=str(core_config['ui']['tray']['updates_icon']) if core_config['ui']['tray']['updates_icon'] else None, + max_width=default_width, + allowed_extensions=allowed_exts) + + sub_comps = [FormComponent([input_update_interval, select_def_icon, select_up_icon], spaces=False)] + return TabComponent(self.i18n['core.config.tab.tray'].capitalize(), PanelComponent(sub_comps), None, 'core.tray') + + def _gen_ui_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: + default_width = floor(0.11 * screen_width) + + select_hdpi = self._gen_bool_component(label=self.i18n['core.config.ui.hdpi'], + tooltip=self.i18n['core.config.ui.hdpi.tip'], + value=bool(core_config['ui']['hdpi']), + max_width=default_width, + id_='hdpi') + + select_ascale = self._gen_bool_component(label=self.i18n['core.config.ui.auto_scale'], + tooltip=self.i18n['core.config.ui.auto_scale.tip'].format('QT_AUTO_SCREEN_SCALE_FACTOR'), + value=bool(core_config['ui']['auto_scale']), + max_width=default_width, + id_='auto_scale') + + cur_style = QApplication.instance().style().objectName().lower() if not core_config['ui']['style'] else core_config['ui']['style'] + style_opts = [InputOption(label=s.capitalize(), value=s.lower()) for s in QStyleFactory.keys()] + select_style = SingleSelectComponent(label=self.i18n['style'].capitalize(), + options=style_opts, + default_option=[o for o in style_opts if o.value == cur_style][0], + type_=SelectViewType.COMBO, + max_width=default_width, + id_="style") + + input_maxd = TextInputComponent(label=self.i18n['core.config.ui.max_displayed'].capitalize(), + tooltip=self.i18n['core.config.ui.max_displayed.tip'].capitalize(), + only_int=True, + id_="table_max", + max_width=default_width, + value=str(core_config['ui']['table']['max_displayed'])) + + select_dicons = self._gen_bool_component(label=self.i18n['core.config.download.icons'], + tooltip=self.i18n['core.config.download.icons.tip'], + id_="down_icons", + max_width=default_width, + value=core_config['download']['icons']) + + sub_comps = [FormComponent([select_hdpi, select_ascale, select_dicons, select_style, input_maxd], spaces=False)] + return TabComponent(self.i18n['core.config.tab.ui'].capitalize(), PanelComponent(sub_comps), None, 'core.ui') + + def _gen_general_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: + default_width = floor(0.11 * screen_width) + + locale_opts = [InputOption(label=self.i18n['locale.{}'.format(k)].capitalize(), value=k) for k in translation.get_available_keys()] + + current_locale = None + + if core_config['locale']: + current_locale = [l for l in locale_opts if l.value == core_config['locale']] + + if not current_locale and self.i18n.default_key: + current_locale = [l for l in locale_opts if l.value == self.i18n.default_key] + + current_locale = current_locale[0] if current_locale else None + + select_locale = SingleSelectComponent(label=self.i18n['core.config.locale.label'], + options=locale_opts, + default_option=current_locale, + type_=SelectViewType.COMBO, + max_width=default_width, + id_='locale') + + select_sysnotify = self._gen_bool_component(label=self.i18n['core.config.system.notifications'].capitalize(), + tooltip=self.i18n['core.config.system.notifications.tip'].capitalize(), + value=bool(core_config['system']['notifications']), + max_width=default_width, + id_="sys_notify") + + select_sugs = self._gen_bool_component(label=self.i18n['core.config.suggestions.activated'].capitalize(), + tooltip=self.i18n['core.config.suggestions.activated.tip'].capitalize(), + id_="sugs_enabled", + max_width=default_width, + value=bool(core_config['suggestions']['enabled'])) + + inp_sugs = TextInputComponent(label=self.i18n['core.config.suggestions.by_type'], + tooltip=self.i18n['core.config.suggestions.by_type.tip'], + value=str(core_config['suggestions']['by_type']), + only_int=True, + max_width=default_width, + id_="sugs_by_type") + + sub_comps = [FormComponent([select_locale, select_sysnotify, select_sugs, inp_sugs], spaces=False)] + return TabComponent(self.i18n['core.config.tab.general'].capitalize(), PanelComponent(sub_comps), None, 'core.gen') + + def _gen_bool_component(self, label: str, tooltip: str, value: bool, id_: str, max_width: int = 200) -> SingleSelectComponent: + opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), + InputOption(label=self.i18n['no'].capitalize(), value=False)] + + return SingleSelectComponent(label=label, + options=opts, + default_option=[o for o in opts if o.value == value][0], + type_=SelectViewType.RADIO, + tooltip=tooltip, + max_per_line=len(opts), + max_width=max_width, + id_=id_) + + def _save_settings(self, general: PanelComponent, + advanced: PanelComponent, + ui: PanelComponent, + tray: PanelComponent, + gems_panel: PanelComponent) -> Tuple[bool, List[str]]: + core_config = config.read_config() + + # general + general_form = general.components[0] + + locale = general_form.get_component('locale').get_selected() + + if locale != self.i18n.current_key: + core_config['locale'] = locale + + core_config['system']['notifications'] = general_form.get_component('sys_notify').get_selected() + core_config['suggestions']['enabled'] = general_form.get_component('sugs_enabled').get_selected() + + sugs_by_type = general_form.get_component('sugs_by_type').get_int_value() + core_config['suggestions']['by_type'] = sugs_by_type + + # advanced + adv_form = advanced.components[0] + core_config['disk_cache']['enabled'] = adv_form.get_component('dcache').get_selected() + + download_mthreaded = adv_form.get_component('down_mthread').get_selected() + core_config['download']['multithreaded'] = download_mthreaded + + single_dep_check = adv_form.get_component('dep_check').get_selected() + core_config['system']['single_dependency_checking'] = single_dep_check + + data_exp = adv_form.get_component('data_exp').get_int_value() + core_config['memory_cache']['data_expiration'] = data_exp + + icon_exp = adv_form.get_component('icon_exp').get_int_value() + core_config['memory_cache']['icon_expiration'] = icon_exp + + # tray + tray_form = tray.components[0] + core_config['updates']['check_interval'] = tray_form.get_component('updates_interval').get_int_value() + + def_icon_path = tray_form.get_component('def_icon').file_path + core_config['ui']['tray']['default_icon'] = def_icon_path if def_icon_path else None + + up_icon_path = tray_form.get_component('up_icon').file_path + core_config['ui']['tray']['updates_icon'] = up_icon_path if up_icon_path else None + + # ui + ui_form = ui.components[0] + + core_config['download']['icons'] = ui_form.get_component('down_icons').get_selected() + core_config['ui']['hdpi'] = ui_form.get_component('hdpi').get_selected() + + previous_autoscale = core_config['ui']['auto_scale'] + + core_config['ui']['auto_scale'] = ui_form.get_component('auto_scale').get_selected() + + if previous_autoscale and not core_config['ui']['auto_scale']: + self.logger.info("Deleting environment variable QT_AUTO_SCREEN_SCALE_FACTOR") + del os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] + + core_config['ui']['table']['max_displayed'] = ui_form.get_component('table_max').get_int_value() + + style = ui_form.get_component('style').get_selected() + + cur_style = core_config['ui']['style'] if core_config['ui']['style'] else QApplication.instance().style().objectName().lower() + if style != cur_style: + core_config['ui']['style'] = style + + # gems + checked_gems = gems_panel.components[1].get_component('gems').get_selected_values() + + for man in self.managers: + modname = man.__module__.split('.')[-2] + enabled = modname in checked_gems + man.set_enabled(enabled) + + core_config['gems'] = None if core_config['gems'] is None and len(checked_gems) == len(self.managers) else checked_gems + + try: + config.save(core_config) + return True, None + except: + return False, [traceback.format_exc()] + + def save_settings(self, component: TabGroupComponent) -> Tuple[bool, List[str]]: + + saved, warnings = True, [] + + success, errors = self._save_settings(general=component.get_tab('core.gen').content, + advanced=component.get_tab('core.adv').content, + tray=component.get_tab('core.tray').content, + ui=component.get_tab('core.ui').content, + gems_panel=component.get_tab('core.types').content) + + if not success: + saved = False + + if errors: + warnings.extend(errors) + + for man in self.managers: + if man: + modname = man.__module__.split('.')[-2] + tab = component.get_tab(modname) + + if not tab: + self.logger.warning("Tab for {} was not found".format(man.__class__.__name__)) + else: + res = man.save_settings(tab.content) + + if res: + success, errors = res[0], res[1] + + if not success: + saved = False + + if errors: + warnings.extend(errors) + + return saved, warnings diff --git a/bauh/view/qt/about.py b/bauh/view/qt/about.py index abc2e3c1..5965a4cd 100644 --- a/bauh/view/qt/about.py +++ b/bauh/view/qt/about.py @@ -1,7 +1,7 @@ from glob import glob -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel, QWidget, QHBoxLayout from bauh import __version__, __app_name__, ROOT_DIR @@ -55,8 +55,8 @@ def __init__(self, i18n: I18n): gems_widget.layout().addWidget(QLabel()) for gem_path in available_gems: icon = QLabel() - pxmap = QPixmap(gem_path + '/resources/img/{}.svg'.format(gem_path.split('/')[-1])) - icon.setPixmap(pxmap.scaled(24, 24, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + icon_path = gem_path + '/resources/img/{}.svg'.format(gem_path.split('/')[-1]) + icon.setPixmap(QIcon(icon_path).pixmap(QSize(25, 25))) gems_widget.layout().addWidget(icon) gems_widget.layout().addWidget(QLabel()) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 985020c7..cc2dda99 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -30,17 +30,18 @@ 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.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.app_view = app_view self.root = root layout = QHBoxLayout() - layout.setContentsMargins(2, 2, 2, 0) + layout.setContentsMargins(0, 0, 0, 0) layout.setAlignment(Qt.AlignCenter) self.setLayout(layout) self.bt = QToolButton() self.bt.setCheckable(clickable) + self.bt.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) if clickable: self.bt.clicked.connect(self.change_state) @@ -195,8 +196,7 @@ def _install_app(self, pkgv: PackageView): self.window.install(pkgv) def _load_icon_and_cache(self, http_response: QNetworkReply): - - icon_url = http_response.url().toString() + icon_url = http_response.request().url().toString() icon_data = self.icon_cache.get(icon_url) icon_was_cached = True @@ -248,26 +248,36 @@ def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, chang col_update = None if update_check_enabled and pkg.model.update: - col_update = UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked) + col_update = QToolBar() + col_update.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + col_update.addWidget(UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked)) self.setCellWidget(pkg.table_index, 7, col_update) def _gen_row_button(self, text: str, style: str, callback) -> QWidget: col = QWidget() - col_bt = QPushButton() + col.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + col_bt = QToolButton() + col_bt.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) col_bt.setText(text) - col_bt.setStyleSheet('QPushButton { ' + style + '}') + col_bt.setStyleSheet('QToolButton { ' + style + '}') + col_bt.setMinimumWidth(80) col_bt.clicked.connect(callback) layout = QHBoxLayout() - layout.setContentsMargins(2, 2, 2, 0) + layout.setContentsMargins(0, 0, 0, 0) layout.setAlignment(Qt.AlignCenter) + layout.addWidget(col_bt) - col.setLayout(layout) + col.setLayout(layout) return col def _set_col_installed(self, col: int, pkg: PackageView): + toolbar = QToolBar() + toolbar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + if pkg.model.installed: if pkg.model.can_be_uninstalled(): def uninstall(): @@ -287,7 +297,8 @@ def install(): else: item = None - self.setCellWidget(pkg.table_index, col, item) + toolbar.addWidget(item) + self.setCellWidget(pkg.table_index, col, toolbar) def _set_col_type(self, col: int, pkg: PackageView): diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 847ac0ae..693ce835 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -1,14 +1,17 @@ +import os from pathlib import Path from typing import Tuple -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QIcon, QPixmap, QIntValidator from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ - QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog + QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \ + QSlider, QScrollArea, QFrame from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ - TextInputComponent, FormComponent, FileChooserComponent -from bauh.view.qt import css, view_utils + TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ + TwoStateButtonComponent, TextComponent, SpacerComponent +from bauh.view.qt import css from bauh.view.util import resource from bauh.view.util.translation import I18n @@ -61,11 +64,30 @@ def _set_checked(self, state): self.callback(self.model, checked) -class ComboBoxQt(QComboBox): +class TwoStateButtonQt(QSlider): + + def __init__(self, model: TwoStateButtonComponent): + super(TwoStateButtonQt, self).__init__(Qt.Horizontal) + self.model = model + self.setMaximum(1) + self.valueChanged.connect(self._change_state) + + def mousePressEvent(self, QMouseEvent): + self.setValue(1 if self.value() == 0 else 0) + + def _change_state(self, state: int): + self.model.state = bool(state) + + +class FormComboBoxQt(QComboBox): def __init__(self, model: SingleSelectComponent): - super(ComboBoxQt, self).__init__() + super(FormComboBoxQt, self).__init__() self.model = model + + if model.max_width > 0: + self.setMaximumWidth(model.max_width) + for idx, op in enumerate(self.model.options): self.addItem(op.label, op.value) @@ -83,6 +105,42 @@ def _set_selected(self, idx: int): self.setToolTip(self.model.value.tooltip) +class FormRadioSelectQt(QWidget): + + def __init__(self, model: SingleSelectComponent, parent: QWidget = None): + super(FormRadioSelectQt, self).__init__(parent=parent) + self.model = model + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + + if model.max_width > 0: + self.setMaximumWidth(model.max_width) + + grid = QGridLayout() + self.setLayout(grid) + + line, col = 0, 0 + for op in model.options: + comp = RadioButtonQt(op, model) + comp.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + comp.setText(op.label) + comp.setToolTip(op.tooltip) + + if model.value and model.value == op: + self.value = comp + comp.setChecked(True) + + grid.addWidget(comp, line, col) + + if col + 1 == self.model.max_per_line: + line += 1 + col = 0 + else: + col += 1 + + if model.max_width <= 0: + self.setMaximumWidth(self.sizeHint().width()) + + class RadioSelectQt(QGroupBox): def __init__(self, model: SingleSelectComponent): @@ -120,7 +178,7 @@ def __init__(self, model: SingleSelectComponent): self.setLayout(QGridLayout()) self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') self.layout().addWidget(QLabel(model.label + ' :'), 0, 0) - self.layout().addWidget(ComboBoxQt(model), 0, 1) + self.layout().addWidget(FormComboBoxQt(model), 0, 1) class TextInputQt(QGroupBox): @@ -132,8 +190,14 @@ def __init__(self, model: TextInputComponent): self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') self.layout().addWidget(QLabel(model.label.capitalize() + ' :' if model.label else ''), 0, 0) + if self.model.max_width > 0: + self.setMaximumWidth(self.model.max_width) + self.text_input = QLineEdit() + if model.only_int: + self.text_input.setValidator(QIntValidator()) + if model.placeholder: self.text_input.setPlaceholderText(model.placeholder) @@ -151,15 +215,6 @@ def __init__(self, model: TextInputComponent): def _update_model(self, text: str): self.model.value = text -# class ComboSelectQt(QGroupBox): -# -# def __init__(self, model: SingleSelectComponent): -# super(ComboSelectQt, self).__init__(model.label + ' :') -# self.model = model -# self.setLayout(QGridLayout()) -# self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') -# self.layout().addWidget(ComboBoxQt(model), 0, 1) - class MultipleSelectQt(QGroupBox): @@ -170,9 +225,16 @@ def __init__(self, model: MultipleSelectComponent, callback): self._layout = QGridLayout() self.setLayout(self._layout) + if model.max_width > 0: + self.setMaximumWidth(model.max_width) + + if model.max_height > 0: + self.setMaximumHeight(model.max_height) + if model.label: line = 1 - self.layout().addWidget(QLabel(), 0, 1) + pre_label = QLabel() + self.layout().addWidget(pre_label, 0, 1) else: line = 0 @@ -212,6 +274,69 @@ def __init__(self, model: MultipleSelectComponent, callback): else: col += 1 + if model.label: + pos_label = QLabel() + self.layout().addWidget(pos_label, line + 1, 1) + + +class FormMultipleSelectQt(QWidget): + + def __init__(self, model: MultipleSelectComponent, parent: QWidget = None): + super(FormMultipleSelectQt, self).__init__(parent=parent) + self.model = model + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + + if model.max_width > 0: + self.setMaximumWidth(model.max_width) + + if model.max_height > 0: + self.setMaximumHeight(model.max_height) + + self._layout = QGridLayout() + self.setLayout(self._layout) + + if model.label: + line = 1 + self.layout().addWidget(QLabel(), 0, 1) + else: + line = 0 + + col = 0 + + pixmap_help = QPixmap() + + for op in model.options: # loads the help icon if at least one option has a tooltip + if op.tooltip: + with open(resource.get_path('img/about.svg'), 'rb') as f: + pixmap_help.loadFromData(f.read()) + pixmap_help = pixmap_help.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation) + break + + for op in model.options: + comp = CheckboxQt(op, model, None) + + if model.values and op in model.values: + self.value = comp + comp.setChecked(True) + + widget = QWidget() + widget.setLayout(QHBoxLayout()) + widget.layout().addWidget(comp) + + if op.tooltip: + help_icon = QLabel() + help_icon.setPixmap(pixmap_help) + help_icon.setToolTip(op.tooltip) + widget.layout().addWidget(help_icon) + + self._layout.addWidget(widget, line, col) + + if col + 1 == self.model.max_per_line: + line += 1 + col = 0 + else: + col += 1 + if model.label: self.layout().addWidget(QLabel(), line + 1, 1) @@ -241,7 +366,7 @@ def setText(self, p_str): class IconButton(QWidget): - def __init__(self, icon: QIcon, action, i18n: I18n, 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, expanding: bool = False): super(IconButton, self).__init__() self.bt = QToolButton() self.bt.setIcon(icon) @@ -249,7 +374,7 @@ def __init__(self, icon: QIcon, action, i18n: I18n, background: str = None, alig self.i18n = i18n self.default_tootip = tooltip self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) - self.bt.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.bt.setSizePolicy(QSizePolicy.Expanding if expanding else QSizePolicy.Minimum, QSizePolicy.Minimum) if background: style = 'QToolButton { color: white; background: ' + background + '} ' @@ -274,6 +399,20 @@ def setEnabled(self, enabled): self.bt.setToolTip(self.default_tootip) +class PanelQt(QWidget): + + def __init__(self, model: PanelComponent, i18n: I18n, parent: QWidget = None): + super(PanelQt, self).__init__(parent=parent) + self.model = model + self.i18n = i18n + self.setLayout(QVBoxLayout()) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + + if model.components: + for c in model.components: + self.layout().addWidget(to_widget(c, i18n)) + + class FormQt(QGroupBox): def __init__(self, model: FormComponent, i18n: I18n): @@ -283,27 +422,66 @@ def __init__(self, model: FormComponent, i18n: I18n): self.setLayout(QFormLayout()) self.setStyleSheet(css.GROUP_BOX) - self.layout().addRow(QLabel(), QLabel()) + if model.spaces: + self.layout().addRow(QLabel(), QLabel()) for c in model.components: if isinstance(c, TextInputComponent): label, field = self._new_text_input(c) self.layout().addRow(label, field) elif isinstance(c, SingleSelectComponent): - label = QLabel(c.label.capitalize() if c.label else '') - field = ComboBoxQt(c) - self.layout().addRow(label, field) + label = self._new_label(c) + field = FormComboBoxQt(c) if c.type == SelectViewType.COMBO else FormRadioSelectQt(c) + self.layout().addRow(label, self._wrap(field, c)) elif isinstance(c, FileChooserComponent): label, field = self._new_file_chooser(c) self.layout().addRow(label, field) + elif isinstance(c, FormComponent): + self.layout().addRow(FormQt(c, self.i18n)) + elif isinstance(c, TwoStateButtonComponent): + label = self._new_label(c) + self.layout().addRow(label, TwoStateButtonQt(c)) + elif isinstance(c, MultipleSelectComponent): + label = self._new_label(c) + self.layout().addRow(label, FormMultipleSelectQt(c)) + elif isinstance(c, TextComponent): + self.layout().addRow(self._new_label(c), QWidget()) else: raise Exception('Unsupported component type {}'.format(c.__class__.__name__)) - self.layout().addRow(QLabel(), QLabel()) + if model.spaces: + self.layout().addRow(QLabel(), QLabel()) + + def _new_label(self, comp) -> QWidget: + label = QWidget() + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + label.setLayout(QHBoxLayout()) + label_comp = QLabel() + label.layout().addWidget(label_comp) + + attr = 'label' if hasattr(comp,'label') else 'value' + text = getattr(comp, attr) + + if text: + label_comp.setText(text.capitalize()) + + if comp.tooltip: + label.layout().addWidget(self.gen_tip_icon(comp.tooltip)) + + return label + + def gen_tip_icon(self, tip: str) -> QLabel: + tip_icon = QLabel() + tip_icon.setToolTip(tip.strip()) + tip_icon.setPixmap(QIcon(resource.get_path('img/about.svg')).pixmap(QSize(12, 12))) + return tip_icon def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: line_edit = QLineEdit() + if c.only_int: + line_edit.setValidator(QIntValidator()) + if c.tooltip: line_edit.setToolTip(c.tooltip) @@ -321,12 +499,42 @@ def update_model(text: str): c.value = text line_edit.textChanged.connect(update_model) - return QLabel(c.label.capitalize() if c.label else ''), line_edit + + label = QWidget() + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + label.setLayout(QHBoxLayout()) + + label_component = QLabel() + label.layout().addWidget(label_component) + + if label: + label_component.setText(c.label.capitalize()) + + if c.tooltip: + label.layout().addWidget(self.gen_tip_icon(c.tooltip)) + + return label, self._wrap(line_edit, c) + + def _wrap(self, comp: QWidget, model: ViewComponent) -> QWidget: + field_container = QWidget() + field_container.setLayout(QHBoxLayout()) + + if model.max_width > 0: + field_container.setMaximumWidth(model.max_width) + + field_container.layout().addWidget(comp) + return field_container def _new_file_chooser(self, c: FileChooserComponent) -> Tuple[QLabel, QLineEdit]: chooser = QLineEdit() chooser.setReadOnly(True) + if c.max_width > 0: + chooser.setMaximumWidth(c.max_width) + + if c.file_path: + chooser.setText(c.file_path) + chooser.setPlaceholderText(self.i18n['view.components.file_chooser.placeholder']) def open_chooser(e): @@ -337,23 +545,52 @@ def open_chooser(e): else: exts = '{}} (*);;'.format(self.i18n['all_files'].capitalize()) - file_path, _ = QFileDialog.getOpenFileName(self, self.i18n['file_chooser.title'], str(Path.home()), exts, options=options) + if c.file_path and os.path.isfile(c.file_path): + cur_path = c.file_path + else: + cur_path = str(Path.home()) + + file_path, _ = QFileDialog.getOpenFileName(self, self.i18n['file_chooser.title'], cur_path, exts, options=options) if file_path: c.file_path = file_path chooser.setText(file_path) - else: - c.file_path = None - chooser.setText('') chooser.setCursorPosition(0) + def clean_path(): + c.file_path = None + chooser.setText('') + chooser.mousePressEvent = open_chooser - return QLabel(c.label if c.label else ''), chooser + label = self._new_label(c) + wrapped = self._wrap(chooser, c) + + bt = IconButton(QIcon(resource.get_path('img/clean.svg')), + i18n=self.i18n['clean'].capitalize(), + action=clean_path, + background='#cc0000', + tooltip=self.i18n['action.run.tooltip']) + + wrapped.layout().addWidget(bt) + return label, wrapped -def new_single_select(model: SingleSelectComponent): +class TabGroupQt(QTabWidget): + + def __init__(self, model: TabGroupComponent, i18n: I18n, parent: QWidget = None): + super(TabGroupQt, self).__init__(parent=parent) + self.model = model + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.setTabPosition(QTabWidget.North) + + for c in model.tabs: + icon = QIcon(c.icon_path) if c.icon_path else QIcon() + self.addTab(to_widget(c.content, i18n), icon, c.label) + + +def new_single_select(model: SingleSelectComponent) -> QWidget: if model.type == SelectViewType.RADIO: return RadioSelectQt(model) elif model.type == SelectViewType.COMBO: @@ -370,3 +607,28 @@ def new_spacer(min_width: int = None) -> QWidget: spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) return spacer + + +def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidget: + if isinstance(comp, SingleSelectComponent): + return new_single_select(comp) + elif isinstance(comp, MultipleSelectComponent): + return MultipleSelectQt(comp, None) + elif isinstance(comp, TextInputComponent): + return TextInputQt(comp) + elif isinstance(comp, FormComponent): + return FormQt(comp, i18n) + elif isinstance(comp, TabGroupComponent): + return TabGroupQt(comp, i18n, parent) + elif isinstance(comp, PanelComponent): + return PanelQt(comp, i18n, parent) + elif isinstance(comp, TwoStateButtonComponent): + return TwoStateButtonQt(comp) + elif isinstance(comp, TextComponent): + label = QLabel(comp.value) + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + return label + elif isinstance(comp, SpacerComponent): + return new_spacer() + else: + raise Exception("Cannot render instances of " + comp.__class__.__name__) diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index c797509e..e3e7a440 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -3,10 +3,9 @@ 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.api.abstract.view import ViewComponent from bauh.view.qt import css -from bauh.view.qt.components import MultipleSelectQt, new_single_select, TextInputQt, FormQt +from bauh.view.qt.components import to_widget from bauh.view.util.translation import I18n @@ -43,17 +42,7 @@ def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, compo height = 0 for idx, comp in enumerate(components): - if isinstance(comp, SingleSelectComponent): - inst = new_single_select(comp) - elif isinstance(comp, MultipleSelectComponent): - inst = MultipleSelectQt(comp, None) - elif isinstance(comp, TextInputComponent): - inst = TextInputQt(comp) - elif isinstance(comp, FormComponent): - inst = FormQt(comp, i18n) - else: - raise Exception("Cannot render instances of " + comp.__class__.__name__) - + inst = to_widget(comp, i18n) height += inst.sizeHint().height() if inst.sizeHint().width() > width: diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py new file mode 100644 index 00000000..2ce98fa2 --- /dev/null +++ b/bauh/view/qt/settings.py @@ -0,0 +1,79 @@ +from io import StringIO + +from PyQt5.QtCore import QSize +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QPushButton + +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.abstract.view import MessageType +from bauh.view.core.controller import GenericSoftwareManager +from bauh.view.qt import dialog, css +from bauh.view.qt.components import to_widget, new_spacer +from bauh.view.util import util +from bauh.view.util.translation import I18n + + +class SettingsWindow(QWidget): + + def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, tray: bool, window: QWidget, parent: QWidget = None): + super(SettingsWindow, self).__init__(parent=parent) + self.setWindowTitle(i18n['settings'].capitalize()) + self.setLayout(QVBoxLayout()) + self.manager = manager + self.i18n = i18n + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.tray = tray + self.window = window + + self.settings_model = self.manager.get_settings(screen_size.width(), screen_size.height()) + + tab_group = to_widget(self.settings_model, i18n) + tab_group.setMinimumWidth(int(screen_size.width() / 3)) + self.layout().addWidget(tab_group) + + action_bar = QToolBar() + action_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + + bt_close = QPushButton() + bt_close.setText(self.i18n['close'].capitalize()) + bt_close.clicked.connect(lambda: self.close()) + action_bar.addWidget(bt_close) + + action_bar.addWidget(new_spacer()) + + bt_change = QPushButton() + bt_change.setStyleSheet(css.OK_BUTTON) + bt_change.setText(self.i18n['change'].capitalize()) + bt_change.clicked.connect(self._save_settings) + action_bar.addWidget(bt_change) + + self.layout().addWidget(action_bar) + + def _save_settings(self): + success, warnings = self.manager.save_settings(self.settings_model) + + # Configurações alteradas com sucesso, porém algumas delas só surtirão após a reinicialização + + if success: + if dialog.ask_confirmation(title=self.i18n['warning'].capitalize(), + body="

{}

{}

".format(self.i18n['settings.changed.success.warning'], + self.i18n['settings.changed.success.reboot']), + i18n=self.i18n): + util.restart_app(self.tray) + else: + if isinstance(self.manager, GenericSoftwareManager): + self.manager.reset_cache() + + self.manager.prepare() + self.window.verify_warnings() + self.window.types_changed = True + self.window.refresh_apps() + self.close() + else: + msg = StringIO() + msg.write("

{}

".format(self.i18n['settings.error'])) + + for w in warnings: + msg.write('

* ' + w + '


') + + msg.seek(0) + dialog.show_message(title="Warning", body=msg.read(), type_=MessageType.WARNING) diff --git a/bauh/view/qt/styles.py b/bauh/view/qt/styles.py deleted file mode 100644 index d092bd76..00000000 --- a/bauh/view/qt/styles.py +++ /dev/null @@ -1,45 +0,0 @@ -from PyQt5.QtWidgets import QComboBox, QStyleFactory, QWidget, QApplication - -from bauh import __app_name__ -from bauh.commons.html import bold -from bauh.view.core import config -from bauh.view.util import util -from bauh.view.qt import dialog -from bauh.view.util.translation import I18n - - -class StylesComboBox(QComboBox): - - def __init__(self, parent: QWidget, i18n: I18n, show_panel_after_restart: bool): - super(StylesComboBox, self).__init__(parent=parent) - self.app = QApplication.instance() - self.styles = [] - self.i18n = i18n - self.last_index = 0 - self.show_panel_after_restart = show_panel_after_restart - - for idx, style in enumerate(QStyleFactory.keys()): - self.styles.append(style) - self.addItem('{}: {}'.format(i18n['style'].capitalize(), style), style) - - if style.lower() == self.app.style().objectName(): - self.setCurrentIndex(idx) - self.last_index = idx - - self.currentIndexChanged.connect(self.change_style) - - def change_style(self, idx: int): - - if dialog.ask_confirmation(self.i18n['style.change.title'], self.i18n['style.change.question'].format(bold(__app_name__)), self.i18n): - self.last_index = idx - style = self.styles[idx] - - user_config = config.read_config() - user_config['ui']['style'] = style - config.save(user_config) - - util.restart_app(self.show_panel_after_restart) - else: - self.blockSignals(True) - self.setCurrentIndex(self.last_index) - self.blockSignals(False) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 50c4bd7a..a4a5bf9c 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -4,9 +4,9 @@ from typing import List, Type, Set from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal -from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \ - QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QMenu, QAction, QApplication, QListView + QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext @@ -16,18 +16,16 @@ 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, 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 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 ask_root_password from bauh.view.qt.screenshots import ScreenshotsDialog -from bauh.view.qt.styles import StylesComboBox +from bauh.view.qt.settings import SettingsWindow from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \ AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots @@ -136,6 +134,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.toolbar = QToolBar() self.toolbar.setStyleSheet('QToolBar {spacing: 4px; margin-top: 15px; margin-bottom: 5px}') + self.toolbar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.checkbox_updates = QCheckBox() self.checkbox_updates.setText(self.i18n['updates'].capitalize()) @@ -153,6 +152,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.combo_filter_type = QComboBox() self.combo_filter_type.setView(QListView()) self.combo_filter_type.setStyleSheet('QLineEdit { height: 2px; }') + self.combo_filter_type.setIconSize(QSize(14, 14)) self.combo_filter_type.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.combo_filter_type.setEditable(True) self.combo_filter_type.lineEdit().setReadOnly(True) @@ -315,17 +315,20 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.toolbar_bottom.addWidget(new_spacer()) - self.combo_styles = StylesComboBox(parent=self, i18n=i18n, show_panel_after_restart=bool(tray_icon)) - self.combo_styles.setStyleSheet('QComboBox {font-size: 12px;}') - self.ref_combo_styles = self.toolbar_bottom.addWidget(self.combo_styles) - bt_settings = IconButton(QIcon(resource.get_path('img/app_settings.svg')), - action=self._show_settings_menu, + action=self._show_settings, background='#12ABAB', i18n=self.i18n, tooltip=self.i18n['manage_window.bt_settings.tooltip']) self.ref_bt_settings = self.toolbar_bottom.addWidget(bt_settings) + bt_about = IconButton(QIcon(resource.get_path('img/question.svg')), + action=self._show_about, + background='#2E68D3', + i18n=self.i18n, + tooltip=self.i18n['manage_window.settings.about']) + self.ref_bt_about = self.toolbar_bottom.addWidget(bt_about) + self.layout.addWidget(self.toolbar_bottom) qt_utils.centralize(self) @@ -345,10 +348,12 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.thread_warnings = ListWarnings(man=manager, i18n=i18n) self.thread_warnings.signal_warnings.connect(self._show_warnings) + self.settings_window = None + self.search_performed = False def set_tray_icon(self, tray_icon): self.tray_icon = tray_icon - self.combo_styles.show_panel_after_restart = bool(tray_icon) + # self.combo_styles.show_panel_after_restart = bool(tray_icon) def _update_process_progress(self, val: int): if self.progress_controll_enabled: @@ -441,6 +446,7 @@ def verify_warnings(self): def _show_installed(self): if self.pkgs_installed: self.finish_action() + self.search_performed = False self.ref_bt_upgrade.setVisible(True) self.ref_checkbox_only_apps.setVisible(True) self.input_search.setText('') @@ -543,6 +549,8 @@ def read_suggestions(self): def _finish_refresh_apps(self, res: dict, as_installed: bool = True): self.finish_action() + self.search_performed = False + self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) self.ref_bt_upgrade.setVisible(True) self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall and res['types']) @@ -580,7 +588,11 @@ def _finish_uninstall(self, pkgv: PackageView): if self._can_notify_user(): util.notify_user('{} ({}) {}'.format(pkgv.model.name, pkgv.model.get_type(), self.i18n['uninstalled'])) - only_pkg_type = len([p for p in self.pkgs if p.model.get_type() == pkgv.model.get_type()]) >= 2 + if not self.search_performed: + only_pkg_type = len([p for p in self.pkgs if p.model.get_type() == pkgv.model.get_type()]) >= 2 + else: + only_pkg_type = False + self.recent_uninstall = True self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None) @@ -787,7 +799,7 @@ def _update_type_filters(self, available_types: dict = None, keep_selected: bool icon = self.cache_type_filter_icons.get(app_type) if not icon: - icon = load_icon(icon_path, 14) + icon = QIcon(icon_path) self.cache_type_filter_icons[app_type] = icon self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type) @@ -908,10 +920,10 @@ def _begin_action(self, action_label: str, keep_search: bool = False, keep_bt_in self.ref_combo_filter_type.setVisible(False) self.ref_combo_categories.setVisible(False) self.ref_bt_settings.setVisible(False) + self.ref_bt_about.setVisible(False) self.thread_animate_progress.stop = False self.thread_animate_progress.start() self.ref_progress_bar.setVisible(True) - self.ref_combo_styles.setVisible(False) self.label_status.setText(action_label + "...") self.ref_bt_upgrade.setVisible(False) @@ -941,7 +953,6 @@ def _begin_action(self, action_label: str, keep_search: bool = False, keep_bt_in self.combo_filter_type.setEnabled(False) def finish_action(self, keep_filters: bool = False): - self.ref_combo_styles.setVisible(True) self.thread_animate_progress.stop = True self.thread_animate_progress.wait(msecs=1000) self.ref_progress_bar.setVisible(False) @@ -950,6 +961,7 @@ def finish_action(self, keep_filters: bool = False): self._change_label_substatus('') self.ref_bt_settings.setVisible(True) + self.ref_bt_about.setVisible(True) self.ref_bt_refresh.setVisible(True) self.checkbox_only_apps.setEnabled(True) @@ -1069,6 +1081,7 @@ def search(self): def _finish_search(self, res: dict): self.finish_action() + self.search_performed = True if not res['error']: self.ref_bt_upgrade.setVisible(False) @@ -1156,28 +1169,10 @@ def _finish_custom_action(self, res: dict): else: self.checkbox_console.setChecked(True) - def show_gems_selector(self): - gem_panel = GemSelectorPanel(window=self, - manager=self.manager, i18n=self.i18n, - config=self.config, - show_panel_after_restart=bool(self.tray_icon)) - gem_panel.show() - - def _show_settings_menu(self): - menu_row = QMenu() - - if isinstance(self.manager, GenericSoftwareManager): - action_gems = QAction(self.i18n['manage_window.settings.gems']) - action_gems.setIcon(self.icon_app) - - action_gems.triggered.connect(self.show_gems_selector) - menu_row.addAction(action_gems) - - action_about = QAction(self.i18n['manage_window.settings.about']) - action_about.setIcon(QIcon(resource.get_path('img/about.svg'))) - action_about.triggered.connect(self._show_about) - menu_row.addAction(action_about) - - menu_row.adjustSize() - menu_row.popup(QCursor.pos()) - menu_row.exec_() + def _show_settings(self): + self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, bool(self.tray_icon), self) + self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4)) + self.settings_window.resize(self.size()) + self.settings_window.adjustSize() + qt_utils.centralize(self.settings_window) + self.settings_window.show() diff --git a/bauh/view/resources/img/clean.svg b/bauh/view/resources/img/clean.svg new file mode 100644 index 00000000..38a92d8f --- /dev/null +++ b/bauh/view/resources/img/clean.svg @@ -0,0 +1,124 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bauh/view/resources/img/question.svg b/bauh/view/resources/img/question.svg new file mode 100644 index 00000000..d6194738 --- /dev/null +++ b/bauh/view/resources/img/question.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/bauh/view/resources/img/tools.svg b/bauh/view/resources/img/tools.svg new file mode 100644 index 00000000..1f501bae --- /dev/null +++ b/bauh/view/resources/img/tools.svg @@ -0,0 +1,97 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 3c25ab48..1277d1e6 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -120,11 +120,8 @@ gem_selector.question=Quins tipus d’aplicacions voleu que es mostrin aquí? proceed=continua change=modifica exit=surt -manage_window.settings.gems=Tipus d’aplicacions style=estil -style.change.title=Canvi d’estil -style.change.question=Cal reiniciar el {} per a canviar-ne l’estil. Voleu continuar? -manage_window.bt_settings.tooltip=Feu clic aquí per a obrir accions addicionals +manage_window.bt_settings.tooltip=Feu clic aquí per mostrar la configuració downloading=S’està baixant console.install_logs.path=Trobareu registres d’instal·lació a {} author=autor @@ -219,4 +216,53 @@ file_chooser.title=Selector de fitxers message.file.not_exist=Fitxer no existeix 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 +user=usuari +example.short=e.g +core.config.tab.advanced=Advanced +core.config.tab.general=General +core.config.tab.ui=Interface +core.config.tab.tray=Tray +core.config.tab.types=Types +core.config.locale.label=language +core.config.disk_cache=Disk cache +core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +core.config.downloads=downloads +core.config.download.icons=Download icons +core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table +core.config.download.multithreaded=Multithreaded download +core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.mem_cache=Memory storage +core.config.mem_cache.data_exp=Data expiration +core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.suggestions.activated=Suggestions +core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.system.notifications=Notifications +core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated +core.config.ui.max_displayed=Applications displayed +core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.updates_icon=Update icon +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.changed.success.reboot=Restart now ? +settings.error=It was not possible to properly change all the settings +locale.en=anglès +locale.es=castellà +locale.pt=portuguès +locale.ca=català +locale.de=alemany +locale.it=italià +interval=interval +installation=instal·lació +download=download +clean=netejar \ No newline at end of file diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index ecadbca5..33b6b03a 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -120,11 +120,8 @@ gem_selector.question=Welche Arten von Anwendungen willst du hier finden? proceed=Weiter change=Verändern exit=Beenden -manage_window.settings.gems=Anwendungsarten style=Stil -style.change.title=Stil ändern -style.change.question=Um den aktuellen Stil zu ändern ist ein Neustart von {} nötig. Fortfahren? -manage_window.bt_settings.tooltip=Für mehr Optionen hier klicken +manage_window.bt_settings.tooltip=Klicken Sie hier, um die Einstellungen anzuzeigen downloading=Herunterladen console.install_logs.path=Logs der Installationen können unter {} gefunden werden author=Autor @@ -174,4 +171,53 @@ message.file.not_exist=Datei existiert nicht message.file.not_exist.body=Die Datei {} scheint nicht zu existieren development=Entwicklung icon_button.tooltip.disabled=Diese Aktion ist nicht verfügbar -user=Benutzer \ No newline at end of file +user=Benutzer +example.short=e.g +core.config.tab.advanced=Advanced +core.config.tab.general=General +core.config.tab.ui=Interface +core.config.tab.tray=Tray +core.config.tab.types=Types +core.config.locale.label=language +core.config.disk_cache=Disk cache +core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +core.config.downloads=downloads +core.config.download.icons=Download icons +core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table +core.config.download.multithreaded=Multithreaded download +core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.mem_cache=Memory storage +core.config.mem_cache.data_exp=Data expiration +core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.suggestions.activated=Suggestions +core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.system.notifications=Notifications +core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated +core.config.ui.max_displayed=Applications displayed +core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.updates_icon=Update icon +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.changed.success.reboot=Restart now ? +settings.error=It was not possible to properly change all the settings +locale.en=englisch +locale.es=spanisch +locale.pt=portugiesisch +locale.ca=Katalanisch +locale.de=deutsch +locale.it=italienisch +interval=intervall +installation=Installation +download=download +clean=reinigen \ No newline at end of file diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 24113690..02f1eee8 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -120,11 +120,8 @@ gem_selector.question=What types of applications do you want to find here ? proceed=proceed change=change exit=exit -manage_window.settings.gems=Application types style=style -style.change.title=Style change -style.change.question=To change the current style is necessary to restart {}. Proceed ? -manage_window.bt_settings.tooltip=Click here to open extra actions +manage_window.bt_settings.tooltip=Click here to display the settings downloading=Downloading console.install_logs.path=Installation logs can be found at {} author=author @@ -178,4 +175,56 @@ message.file.not_exist=File does not exist message.file.not_exist.body=The file {} seems not to exist development=development icon_button.tooltip.disabled=This action is unavailable -user=user \ No newline at end of file +user=user +example.short=e.g +core.config.tab.advanced=Advanced +core.config.tab.general=General +core.config.tab.ui=Interface +core.config.tab.tray=Tray +core.config.tab.types=Types +core.config.locale.label=language +core.config.disk_cache=Disk cache +core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +core.config.download.icons=Download icons +core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table +core.config.download.multithreaded=Multithreaded download +core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.mem_cache=Memory storage +core.config.mem_cache.data_exp=Data expiration +core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.mem_cache.icon_exp=Icons expiration +core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) +core.config.suggestions.activated=Suggestions +core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.system.notifications=Notifications +core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated +core.config.ui.max_displayed=Applications displayed +core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray +core.config.ui.tray.updates_icon=Update icon +core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.changed.success.reboot=Restart now ? +settings.error=It was not possible to properly change all the settings +locale.en=english +locale.es=spanish +locale.pt=portuguese +locale.ca=catalan +locale.de=german +locale.it=italian +interval=interval +installation=installation +download=download +clean=clean \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index e79ac6fb..2abf90c2 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -120,11 +120,8 @@ gem_selector.question=¿Qué tipos de aplicaciones quiere encontrar aquí? proceed=continuar change=cambiar exit=salir -manage_window.settings.gems=Tipos de aplicaciones style=estilo -style.change.title=Cambio de estilo -style.change.question=Para cambiar el estilo actual es necesario reiniciar {}. ¿Quiere proceder? -manage_window.bt_settings.tooltip=Pulse aquí para abrir acciones adicionales +manage_window.bt_settings.tooltip=Pulse aquí para exhibir las configuraciones downloading=Descargando console.install_logs.path=Los registros de instalación pueden encontrarse en {} author=autor @@ -218,4 +215,57 @@ file_chooser.title=Selector de archivos message.file.not_exist=Archivo no existe 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 +user=usuario +example.short=p.ej +core.config.tab.advanced=Avanzadas +core.config.tab.general=Generales +core.config.tab.ui=Interfaz +core.config.tab.tray=Bandeja +core.config.tab.types=Tipos +core.config.locale.label=idioma +core.config.disk_cache=Cache de disco +core.config.disk_cache.tip=Algunos datos sobre las aplicaciones instaladas serán almacenados en el disco para que puedan cargarse rápidamente en las siguientes inicializaciones +core.config.updates.interval=Intervalo de actualizaciones +core.config.updates.interval.tip=Define el intervalo de tiempo en el que se realizará la verificación de actualizaciones para las aplicaciones instaladas ( en SEGUNDOS ) +core.config.downloads=downloads +core.config.download.icons=Descargar iconos +core.config.download.icons.tip=Si los íconos de las aplicaciones se deben descargar y mostrar en la tabla +core.config.download.multithreaded=Descarga segmentada +core.config.download.multithreaded.tip=Si las aplicaciones, paquetes y archivos deben descargarse con una herramienta que usa segmentación / threads ( más rápido ). Por el momento, esta configuración solo funcionará si el paquete aria2 esté instalado +core.config.mem_cache=Almacenamiento de memoria +core.config.mem_cache.data_exp=Expiración de datos +core.config.mem_cache.data_exp.tip=Define la vida útil de los datos en memoria ( en SEGUNDOS ) +core.config.mem_cache.icon_exp=Expiración de íconos +core.config.mem_cache.icon_exp.tip=Define la vida útil de los íconos en memoria ( en SEGUNDOS ) +core.config.suggestions.activated=Suggestions +core.config.suggestions.activated.tip=Si se pueden sugerir nuevas aplicaciones para instalación +core.config.system.notifications=Notificaciones +core.config.system.notifications.tip=Si notificaciones deben mostrarse cuando finaliza una acción o hay actualizaciones +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=Si se deben activar las mejoras relacionadas con las resoluciones HDPI +core.config.ui.max_displayed=Aplicaciones mostradas +core.config.ui.max_displayed.tip=Número máximo de aplicaciones que se muestran en la tabla +core.config.ui.tray.default_icon=Ícono predeterminado +core.config.ui.tray.default_icon.tip=El icono predeterminado que se muestra en la bandeja +core.config.ui.tray.updates_icon=Icono de actualización +core.config.ui.tray.updates_icon.tip=El icono que se muestra cuando hay actualizaciones disponibles +core.config.system.dep_checking=Verificación única de sistema +core.config.system.dep_checking.tip=Si la verificación de disponibilidad de las tecnologías de su sistema debe ocurrir solo una vez +core.config.suggestions.by_type=Sugerencias por tipo +core.config.suggestions.by_type.tip=Número máximo de sugerencias que deberían mostrarse por tipo de aplicación +core.config.types.tip=Marque los tipos de aplicaciones que desea administrar +core.config.ui.auto_scale=Escala automática +core.config.ui.auto_scale.tip=Activa el factor de escala de pantalla automática ( {} ). Soluciona problemas de escala para algunos ambientes desktop. +settings.changed.success.warning=Las configuraciones se cambiaron con éxito. Algunas solo tendrán efecto después del reinicio. +settings.changed.success.reboot=¿Reiniciar ahora? +settings.error=No fue posible cambiar correctamente todas las configuraciones +locale.es=inglés +locale.es=español +locale.pt=portugués +locale.ca=catalán +locale.de=alemán +locale.it=italiano +interval=intervalo +installation=instalación +download=descarga +clean=limpiar \ No newline at end of file diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 75967b75..4c599b40 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -120,11 +120,8 @@ gem_selector.question=Quali tipi di applicazioni vuoi trovare qui ? proceed=procedere change=Cambia exit=esci -manage_window.settings.gems=Tipi di applicazione style=stile -style.change.title=Cambia stile -style.change.question=Per modificare lo stile corrente è necessario riavviare {}. Procedere ? -manage_window.bt_settings.tooltip=Fai clic qui per aprire ulteriori azioni +manage_window.bt_settings.tooltip=Fai clic qui per visualizzare le impostazioni downloading=Scaricamento console.install_logs.path=I registri di installazione sono disponibili all'indirizzo {} author=autore @@ -175,4 +172,52 @@ message.file.not_exist=File non esiste message.file.not_exist.body=Il file {} sembra non esistere development=sviluppo icon_button.tooltip.disabled=Questa azione non è disponibile -user=utente \ No newline at end of file +user=utente +example.short=e.g +core.config.tab.advanced=Advanced +core.config.tab_label=general +core.config.tab.ui=Interface +core.config.tab.types=Types +core.config.locale.label=language +core.config.disk_cache=Disk cache +core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +core.config.downloads=downloads +core.config.download.icons=Download icons +core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table +core.config.download.multithreaded=Multithreaded download +core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.mem_cache=Memory storage +core.config.mem_cache.data_exp=Data expiration +core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.suggestions.activated=Suggestions +core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.system.notifications=Notifications +core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated +core.config.ui.max_displayed=Applications displayed +core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.updates_icon=Update icon +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.changed.success.reboot=Restart now ? +settings.error=It was not possible to properly change all the settings +locale.en=inglese +locale.es=spagnolo +locale.pt=portoghese +locale.ca=catalan +locale.de=tedesco +locale.it=italiano +interval=intervallo +installation=installazione +download=download +clean=pulire \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 15390715..52798c48 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -120,11 +120,8 @@ gem_selector.question=Quais tipos de aplicativos você quer encontrar por aqui ? proceed=continuar change=alterar exit=sair -manage_window.settings.gems=Tipos de aplicativos style=estilo -style.change.title=Mudança de estilo -style.change.question=Para alterar o estilo atual é necessário reiniciar o {}. Continuar ? -manage_window.bt_settings.tooltip=Clique aqui para abrir ações adicionais +manage_window.bt_settings.tooltip=Clique aqui para exibir as configurações downloading=Baixando console.install_logs.path=Os registros de instalação podem ser encontrados em {} author=autor @@ -221,4 +218,57 @@ file_chooser.title=Seletor arquivos message.file.not_exist=Arquivo não existe 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 +user=usuário +example.short=ex +core.config.tab.advanced=Avançadas +core.config.tab.general=Gerais +core.config.tab.ui=Interface +core.config.tab.tray=Bandeja +core.config.tab.types=Tipos +core.config.locale.label=idioma +core.config.disk_cache=Cache em disco +core.config.disk_cache.tip=Alguns dados sobre os aplicativos instalados serão armazenados em disco para que os mesmos sejam carregados rapidamente nas próximas inicializações +core.config.updates.interval=Intervalo de atualizações +core.config.updates.interval.tip=Define o intervalo de tempo em que ocorrerá a verificação de atualizações para os aplicativos instalados ( em SEGUNDOS ) +core.config.downloads=downloads +core.config.download.icons=Baixar ícones +core.config.download.icons.tip=Se os ícones dos aplicativos devem ser baixados e exibidos na tabela +core.config.download.multithreaded=Download segmentado +core.config.download.multithreaded.tip=Se os aplicativos, pacotes e arquivos devem ser baixados através de uma ferramenta que trabalha com segmentação / threads ( mais rápido ). No momento esta propriedade somente funcionará se o pacote aria2 estiver instalado. +core.config.mem_cache=Armazenamento em memória +core.config.mem_cache.data_exp=Expiração dos dados +core.config.mem_cache.data_exp.tip=Define o tempo de vida dos dados em memória ( em SEGUNDOS ) +core.config.mem_cache.icon_exp=Expiração de ícones +core.config.mem_cache.icon_exp.tip=Define o tempo de vida dos ícones em memória ( em SEGUNDOS ) +core.config.suggestions.activated=Sugestões +core.config.suggestions.activated.tip=Se novos aplicativos podem ser sugeridos para instalação +core.config.system.notifications=Notificações +core.config.system.notifications.tip=Se notificações devem ser exibidas quando uma ação é finalizada ou existem atualizações +core.config.ui.hdpi=HDPI +core.config.ui.hdpi.tip=Se melhorias para resoluções HDPI devem ser ativadas +core.config.ui.max_displayed=Aplicativos exibidos +core.config.ui.max_displayed.tip=Número máximo de aplicativos exibidos na tabela +core.config.ui.tray.default_icon=Ícone padrão +core.config.ui.tray.default_icon.tip=O ícone padrão exibido na bandeja +core.config.ui.tray.updates_icon=Ícone de atualização +core.config.ui.tray.updates_icon.tip=O ícone exibido quando há atualizações disponíveis +core.config.system.dep_checking=Verificação única de sistema +core.config.system.dep_checking.tip=Se a verificação da disponibilidade das tecnologias do seu sistema devem ocorrer somente uma vez +core.config.suggestions.by_type=Sugestões por tipo +core.config.suggestions.by_type.tip=Número máximo de sugestões que devem ser exibidas por tipo de aplicativo +core.config.types.tip=Marque os tipos de aplicativo que você quer gerenciar +core.config.ui.auto_scale=Escala automática +core.config.ui.auto_scale.tip=Ativa o fator de escala automático ( {} ). Corrige problemas de escala para alguns ambientes desktop. +settings.changed.success.warning=Configurações alteradas com sucesso ! Algumas delas só surtirão após a reinicialização. +settings.changed.success.reboot=Reiniciar agora ? +settings.error=Não foi possível alterar todas as configurações adequadamente +locale.en=inglês +locale.es=espanhol +locale.pt=português +locale.ca=catalão +locale.de=alemão +locale.it=italiano +interval=intervalo +installation=instalação +download=download +clean=limpar \ No newline at end of file diff --git a/bauh/view/util/translation.py b/bauh/view/util/translation.py index 40c22685..0074a2b6 100644 --- a/bauh/view/util/translation.py +++ b/bauh/view/util/translation.py @@ -1,6 +1,9 @@ import glob import locale -from typing import Tuple +import traceback +from typing import Tuple, Set + +from colorama import Fore from bauh.view.util import resource @@ -38,6 +41,11 @@ def get(self, *args, **kwargs): return res +def get_available_keys() -> Set[str]: + locale_dir = resource.get_path('locale') + return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*')} + + def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: locale_path = None @@ -65,8 +73,9 @@ def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale locale_obj = {} for line in locale_keys: - if line: - keyval = line.strip().split('=') + line_strip = line.strip() + if line_strip: + keyval = line_strip.split('=') locale_obj[keyval[0].strip()] = keyval[1].strip() return locale_path.split('/')[-1], locale_obj