diff --git a/.gitignore b/.gitignore index c9e6723fd..2c17b5015 100755 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,6 @@ linux_dist/appimage/appimage-builder-cache linux_dist/appimage/*.zsync linux_dist/appimage/*.AppImage linux_dist/appimage/*.gz +linux_dist/appimage/*.gz.* linux_dist/appimage/bauh-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e0f9cc5..af1dbf1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,53 @@ 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.10.6] 2023-12-23 +### Features + - new **verified** filter for the management table + +### Improvements +- AppImage + - supporting AppImages from GitLab repositories + - info window: displaying "unknown" when there is no mapped license +- Arch + - parallelized installed packages reading + - parallelized search + - adding the AUR's URL on the package information dialog [#339](https://github.com/vinifmor/bauh/issues/339) +- General + - new download implementation replaces `wget` (implemented in Python using the `requests` library) + - replaced the internet checking old host (`google.com`) by a more general one (`w3.org`) [#300](https://github.com/vinifmor/bauh/issues/300) + - faster exit (threaded calls won't be waited) + - adding a new project definition/setup (`pyproject.toml`) file to comply with the latest standards +- UI + - faster package icons download + - faster packages filtering (`type`, `category`, `name`, etc... up to **95% less time**) + - the "Skip" button on the initialization panel is now enabled after 10 seconds [#310](https://github.com/vinifmor/bauh/issues/310) + - displaying the download progress for screenshots + - on the package information dialog is now possible to open fields associated with URLs in the browser [#340](https://github.com/vinifmor/bauh/issues/340) + - displaying a text warning before installing an unverified package (unverified = not verified by the system maintainers or a trustable source) + - at the moment the following packaging formats are considered completely **unverified**: AppImage, AUR, Web + - Snap supports both verified and unverified software + - minor reduction in the table loading time + - improvements to help with random widget centralisation issues + - more translations + +### Fixes +- AppImage + - upgrade fails when the package was initially imported, but later available on bauh's database [#321](https://github.com/vinifmor/bauh/issues/321) +- General + - random segmentation fault errors associated with threads and caching +- Web + - search not working for some typed addresses (e.g: those returning 403) + +### Contributions +- German translations by [Mape6](https://github.com/Mape6) +- Russian translations by [KoromeloDev](https://github.com/KoromeloDev) +- Fix [#329](https://github.com/vinifmor/bauh/issues/329) by [w568w](https://github.com/w568w) + +### Distribution +- bauh's AppImage is now based on Debian bullseye and had a small size reduction + + ## [0.10.5] 2022-12-17 ### Fixes diff --git a/LICENSE b/LICENSE index b3fc2d4a3..b9f066892 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - Copyright (c) 2019-2023 Vinícius Moreira + Copyright (c) 2019 Vinícius Moreira zlib/libpng license diff --git a/MANIFEST.in b/MANIFEST.in index f75b1cd09..73d75a301 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ -include LICENSE CHANGELOG.md README.md -include requirements.txt +include LICENSE CHANGELOG.md README.md requirements.txt recursive-include bauh/desktop * +recursive-exclude bauh/desktop *.orig recursive-include bauh/view/resources * +recursive-exclude bauh/view/resources *.orig recursive-include bauh/gems/*/resources * -recursive-include tests * +recursive-exclude bauh/gems/*/resources *.orig diff --git a/README.md b/README.md index edec81c50..ebfaf99aa 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,16 @@ Key features - `qt5dxcb-plugin` (or equivalent): the package name may vary from distribution ##### Steps -- Download the .AppImage file attached with the latest release from https://github.com/vinifmor/bauh/releases. -- Run the following command through a terminal: `chmod a+x bauh-${version}-x86_64.AppImage` (replace `${version}` by the respective downloaded version) -- Launch it: `./bauh-${version}-x86_64.AppImage` +- Download the .AppImage file attached with the latest release from https://github.com/vinifmor/bauh/releases +- Generally desktop environment allow you to double-click the downloaded file to execute it. If that's not your case, follow the steps below from a console: + - `chmod a+x bauh-${version}-x86_64.AppImage` (replace `${version}` by the respective downloaded version) + - `./bauh-${version}-x86_64.AppImage` +- If you want to integrate bauh to your desktop: click on bauh's settings menu ("sandwich") and then `Install bauh` + +

+ +

+ #### Ubuntu 20.04 based distros (Linux Mint, PopOS, ...) @@ -77,11 +84,12 @@ Key features - `aria2`: multi-threaded downloads - `axel`: multi-threaded downloads alternative - `libappindicator3-1`: tray-mode -- `wget`, `sqlite3`, `fuse`: AppImage support +- `sqlite3`, `fuse`: AppImage support - `flatpak`: Flatpaks support - `snapd`: Snaps support - `python3-lxml`, `python3-bs4`: Web apps support - `python3-venv`: [isolated installation](#inst_iso) +- `xdg-utils`: to open URLs in the browsers (`xdg-open`) ##### Updating bauh @@ -125,8 +133,9 @@ makepkg -si - `aria2`: multi-threaded downloads - `axel`: multi-threaded downloads alternative - `libappindicator-gtk2`: tray-mode (GTK2 desktop environments) -- `libappindicator-gtk3`: tray-mode (GTK3 desktop environments) -- `wget`, `sqlite`, `fuse2`, `fuse3`: AppImage support +- `libappindicator-gtk3`: tray-mode (GTK3 desktop environments) +- `xdg-utils`: to open URLs in the browser (`xdg-open`) +- `sqlite`, `fuse2`, `fuse3`: AppImage support - `flatpak`: Flatpak support - `snapd`: Snap support - `pacman`: ArchLinux package management support diff --git a/bauh/__init__.py b/bauh/__init__.py index f8fef96bf..ee5e95c4c 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.10.5' +__version__ = '0.10.6' __app_name__ = 'bauh' import os diff --git a/bauh/api/abstract/download.py b/bauh/api/abstract/download.py index eead54d74..1eef87d49 100644 --- a/bauh/api/abstract/download.py +++ b/bauh/api/abstract/download.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Iterable, List, Optional +from typing import Iterable, List, Optional, Tuple from bauh.api.abstract.handler import ProcessWatcher @@ -31,7 +31,7 @@ def can_work(self) -> bool: pass @abstractmethod - def get_supported_multithreaded_clients(self) -> Iterable[str]: + def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: pass @abstractmethod @@ -39,9 +39,9 @@ def is_multithreaded_client_available(self, name: str) -> bool: pass @abstractmethod - def list_available_multithreaded_clients(self) -> List[str]: + def list_available_multithreaded_clients(self) -> Tuple[str, ...]: pass @abstractmethod - def get_supported_clients(self) -> tuple: + def get_supported_clients(self) -> Tuple[str, ...]: pass diff --git a/bauh/api/http.py b/bauh/api/http.py index 882b81f3f..f325acb04 100644 --- a/bauh/api/http.py +++ b/bauh/api/http.py @@ -19,14 +19,15 @@ def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = self.sleep = sleep self.logger = logger - def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, session: bool = True) -> Optional[requests.Response]: + def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, + session: bool = True, stream: bool = False) -> Optional[requests.Response]: cur_attempts = 1 while cur_attempts <= self.max_attempts: cur_attempts += 1 try: - args = {'timeout': self.timeout, 'allow_redirects': allow_redirects} + args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream} if params: args['params'] = params @@ -42,7 +43,7 @@ def get(self, url: str, params: dict = None, headers: dict = None, allow_redirec else: res = requests.get(url, **args) - if res.status_code == 200: + if 200 <= res.status_code < 300: return res if single_call: diff --git a/bauh/commons/internet.py b/bauh/commons/internet.py index 4245191c7..e52f17021 100644 --- a/bauh/commons/internet.py +++ b/bauh/commons/internet.py @@ -11,7 +11,7 @@ def is_available(self) -> bool: return False try: - socket.gethostbyname('google.com') + socket.gethostbyname("w3.org") return True except Exception: return False diff --git a/bauh/commons/regex.py b/bauh/commons/regex.py new file mode 100644 index 000000000..f2fecbe21 --- /dev/null +++ b/bauh/commons/regex.py @@ -0,0 +1,3 @@ +import re + +RE_URL = re.compile(r"^https?://.+$") diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index b25d09954..123707e6d 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -79,7 +79,7 @@ def __init__(self, context: ApplicationContext): self.configman = AppImageConfigManager() self._custom_actions: Optional[Iterable[CustomSoftwareAction]] = None self._action_self_install: Optional[CustomSoftwareAction] = None - self._app_github: Optional[str] = None + self._app_repository: Optional[str] = None self._search_unfilled_attrs: Optional[Tuple[str, ...]] = None self._suggestions_downloader: Optional[AppImageSuggestionsDownloader] = None @@ -122,7 +122,7 @@ def install_file(self, root_password: Optional[str], watcher: ProcessWatcher) -> else: return False - appim = AppImage(i18n=self.i18n, imported=True) + appim = AppImage(i18n=self.i18n, imported=True, manual_update=True) appim.name = input_name.get_value().strip() appim.local_file_path = file_chooser.file_path appim.version = input_version.get_value() @@ -168,6 +168,7 @@ def update_file(self, pkg: AppImage, root_password: Optional[str], watcher: Proc pkg.local_file_path = file_chooser.file_path pkg.version = input_version.get_value() + pkg.manual_update = True reqs = UpgradeRequirements(to_install=None, to_remove=None, to_upgrade=[UpgradeRequirement(pkg=pkg)], cannot_upgrade=None) return self.upgrade(reqs, root_password=root_password, watcher=watcher) @@ -183,7 +184,7 @@ def _get_db_connection(self, db_path: str) -> sqlite3.Connection: self.logger.warning(f"Could not get a connection for database '{db_path}'") def _gen_app_key(self, app: AppImage): - return f"{app.name.lower()}{app.github.lower() if app.github else ''}" + return f"{app.name.lower()}{app.repository.lower() if app.repository else ''}" def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: if is_url: @@ -275,7 +276,7 @@ def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1 for tup in cursor.fetchall(): for app in installed_apps: - if app.name.lower() == tup[0].lower() and (not app.github or app.github.lower() == tup[1].lower()): + if app.name.lower() == tup[0].lower() and (not app.repository or app.repository.lower() == tup[1].lower()): continuous_version = app.version == 'continuous' continuous_update = tup[2] == 'continuous' @@ -368,6 +369,16 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: Optional[str download_data = None + # always changing the 'imported' field based on the 'manual_update' flag that means + # "the user is manually installing/updating the AppImage" + if not req.pkg.manual_update: + req.pkg.imported = False + + if req.pkg.categories and "Imported" in req.pkg.categories: + # removing the imported category in case the file is not considered imported anymore + req.pkg.categories.remove("Imported") + + # manual file updates do not required download it if not req.pkg.imported: download_data = self._download(req.pkg, watcher) @@ -430,7 +441,7 @@ def uninstall(self, pkg: AppImage, root_password: Optional[str], watcher: Proces return TransactionResult(success=True, installed=None, removed=[pkg]) def _add_self_latest_version(self, app: AppImage): - if app.name == self.context.app_name and app.github == self.app_github and not app.url_download_latest_version: + if app.name == self.context.app_name and app.repository == self.app_repository and not app.url_download_latest_version: history = self.get_history(app) if not history or not history.history: @@ -469,6 +480,9 @@ def get_info(self, pkg: AppImage) -> dict: if data.get('symlink') and not os.path.islink(data['symlink']): del data['symlink'] + if not data.get("license"): + data["license"] = self.i18n["unknown"].lower() + return data def _sort_release(self, rel: tuple): @@ -486,7 +500,7 @@ def get_history(self, pkg: AppImage) -> PackageHistory: try: cursor = app_con.cursor() - cursor.execute(query.FIND_APP_ID_BY_NAME_AND_GITHUB.format(pkg.name.lower(), pkg.github.lower() if pkg.github else '')) + cursor.execute(query.FIND_APP_ID_BY_REPO_AND_NAME.format(pkg.repository.lower() if pkg.repository else '', pkg.name.lower())) app_tuple = cursor.fetchone() if not app_tuple: @@ -593,8 +607,8 @@ def _install(self, pkg: AppImage, watcher: ProcessWatcher, pre_downloaded_file: Path(out_dir).mkdir(parents=True, exist_ok=True) pkg.install_dir = out_dir - if pkg.imported: - + # when the package is being imported/upgraded there is no need to download it + if pkg.manual_update: downloaded, file_name = True, pkg.local_file_path.split('/')[-1] install_file_path = out_dir + '/' + file_name @@ -725,10 +739,6 @@ def can_work(self) -> Tuple[bool, Optional[str]]: if not self._is_sqlite3_available(): return False, self.i18n['missing_dep'].format(dep=bold('sqlite3')) - if not self.file_downloader.can_work(): - download_clients = ', '.join(self.file_downloader.get_supported_clients()) - return False, self.i18n['appimage.missing_downloader'].format(clients=download_clients) - return True, None def requires_root(self, action: SoftwareAction, pkg: AppImage) -> bool: @@ -984,7 +994,7 @@ def update_database(self, root_password: Optional[str], watcher: ProcessWatcher) @property def search_unfilled_attrs(self) -> Tuple[str, ...]: if self._search_unfilled_attrs is None: - self._search_unfilled_attrs = ('icon_url', 'url_download_latest_version', 'author', 'license', 'github', + self._search_unfilled_attrs = ('icon_url', 'url_download_latest_version', 'author', 'license', 'repository', 'source', 'url_screenshot') return self._search_unfilled_attrs @@ -999,7 +1009,7 @@ def self_install(self, root_password: Optional[str], watcher: ProcessWatcher) -> return False app = AppImage(name=self.context.app_name, version=self.context.app_version, - categories=['system'], author=self.context.app_name, github=self.app_github, + categories=['system'], author=self.context.app_name, repository=self.app_repository, license='zlib/libpng') res = self._install(pkg=app, watcher=watcher, @@ -1080,11 +1090,11 @@ def action_self_install(self) -> CustomSoftwareAction: return self._action_self_install @property - def app_github(self) -> str: - if self._app_github is None: - self._app_github = f'vinifmor/{self.context.app_name}' + def app_repository(self) -> str: + if self._app_repository is None: + self._app_repository = f"https://github.com/vinifmor/{self.context.app_name}" - return self._app_github + return self._app_repository @property def suggestions_downloader(self) -> AppImageSuggestionsDownloader: diff --git a/bauh/gems/appimage/model.py b/bauh/gems/appimage/model.py index 7afa291f3..927300c21 100644 --- a/bauh/gems/appimage/model.py +++ b/bauh/gems/appimage/model.py @@ -32,7 +32,8 @@ def actions_local_installation(cls) -> Tuple[CustomSoftwareAction, ...]: def cached_attrs(cls) -> Tuple[str, ...]: if cls.__cached_attrs is None: cls.__cached_attrs = ('name', 'description', 'version', 'url_download', 'author', 'license', 'source', - 'icon_path', 'github', 'categories', 'imported', 'install_dir', 'symlink') + 'icon_path', 'repository', 'categories', 'imported', 'install_dir', + 'symlink') return cls.__cached_attrs @@ -43,17 +44,17 @@ def re_many_spaces(cls) -> Pattern: return cls.__re_many_spaces - def __init__(self, name: str = None, description: str = None, github: str = None, source: str = None, version: str = None, + def __init__(self, name: str = None, description: str = None, repository: str = None, source: str = None, version: str = None, url_download: str = None, url_icon: str = None, url_screenshot: str = None, license: str = None, author: str = None, categories=None, icon_path: str = None, installed: bool = False, url_download_latest_version: str = None, local_file_path: str = None, imported: bool = False, i18n: I18n = None, install_dir: str = None, updates_ignored: bool = False, - symlink: str = None, **kwargs): + symlink: str = None, manual_update: bool = False, github: str = None, **kwargs): super(AppImage, self).__init__(id=name, name=name, version=version, latest_version=version, icon_url=url_icon, license=license, description=description, installed=installed) self.source = source - self.github = github + self.repository = repository if repository else (github if github else repository) self.categories = (categories.split(',') if isinstance(categories, str) else categories) if categories else None self.url_download = url_download self.icon_path = icon_path @@ -66,9 +67,10 @@ def __init__(self, name: str = None, description: str = None, github: str = None self.install_dir = install_dir self.updates_ignored = updates_ignored self.symlink = symlink + self.manual_update = manual_update # True when the user is manually installing/upgrading an AppImage file def __repr__(self): - return "{} (name={}, github={})".format(self.__class__.__name__, self.name, self.github) + return f"{self.__class__.__name__} (name={self.name}, repository={self.repository})" def can_be_installed(self): return not self.installed and self.url_download diff --git a/bauh/gems/appimage/query.py b/bauh/gems/appimage/query.py index 6c39c5578..b641b3385 100644 --- a/bauh/gems/appimage/query.py +++ b/bauh/gems/appimage/query.py @@ -1,10 +1,12 @@ -APP_ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'url_screenshot', 'license', 'author', 'categories') +APP_ATTRS = ('name', 'description', 'repository', 'source', 'version', 'url_download', 'url_icon', + 'url_screenshot', 'license', 'author', 'categories') RELEASE_ATTRS = ('version', 'url_download', 'published_at') -SEARCH_APPS_BY_NAME_OR_DESCRIPTION = "SELECT {} FROM apps".format(','.join(APP_ATTRS)) + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" -FIND_APP_ID_BY_NAME_AND_GITHUB = "SELECT id FROM apps WHERE lower(name) = '{}' and lower(github) = '{}'" -FIND_APPS_BY_NAME = "SELECT name, github, version, url_download FROM apps WHERE lower(name) IN ({})" +SEARCH_APPS_BY_NAME_OR_DESCRIPTION = f"SELECT {','.join(APP_ATTRS)} FROM apps" + \ + " WHERE lower(name) LIKE '%{}%' or lower(description) LIKE '%{}%'" +FIND_APP_ID_BY_REPO_AND_NAME = "SELECT id FROM apps WHERE lower(repository) = '{}' and lower(name) = '{}'" +FIND_APPS_BY_NAME = "SELECT name, repository, version, url_download FROM apps WHERE lower(name) IN ({})" FIND_APPS_BY_NAME_ONLY_NAME = "SELECT name FROM apps WHERE lower(name) IN ({})" FIND_APPS_BY_NAME_FULL = "SELECT {} FROM apps".format(','.join(APP_ATTRS)) + " WHERE lower(name) IN ({})" FIND_RELEASES_BY_APP_ID = "SELECT {} FROM releases".format(','.join(RELEASE_ATTRS)) + " WHERE app_id = {}" diff --git a/bauh/gems/appimage/resources/__init__.py b/bauh/gems/appimage/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/appimage/resources/img/__init__.py b/bauh/gems/appimage/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/appimage/resources/locale/__init__.py b/bauh/gems/appimage/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca index 49bc7487b..f726bc89d 100644 --- a/bauh/gems/appimage/resources/locale/ca +++ b/bauh/gems/appimage/resources/locale/ca @@ -33,6 +33,7 @@ appimage.info.icon_path=icona appimage.info.imported.false=No appimage.info.imported.true=Yes appimage.info.install_dir=Installation directory +appimage.info.repository=Repositori appimage.info.symlink=Symlink appimage.info.url_download=URL del fitxer 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}. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=S’està creant una drecera del menú appimage.install.extract=S’està extraient el contingut de {} appimage.install.imported.rename_error=It was not possible to move the file {} to {} appimage.install.permission=S’està concedint el permís d’execució a {} -appimage.missing_downloader=No download client installed ({clients}) appimage.update_database.deleting_old=Removing old files appimage.update_database.downloading=Downloading database files appimage.update_database.uncompressing=Uncompressing files diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de index 631ab3e93..a7221a622 100644 --- a/bauh/gems/appimage/resources/locale/de +++ b/bauh/gems/appimage/resources/locale/de @@ -33,6 +33,7 @@ appimage.info.icon_path=Symbol appimage.info.imported.false=Nein appimage.info.imported.true=Ja appimage.info.install_dir=Installationsverzeichnis +appimage.info.repository=Repository appimage.info.symlink=Symlink appimage.info.url_download=Datei-URL appimage.install.appimagelauncher.error={appimgl} lässt nicht zu, dass {app} installiert wird. Deinstallieren Sie {appimgl}, starten Sie Ihr System neu und versuchen Sie erneut, {app} zu installieren. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Ein Tastenkürzel für das Menü festlegen appimage.install.extract=Extrahieren des Inhalts von {} appimage.install.imported.rename_error=Es war nicht möglich, die Datei {} nach {} zu verschieben appimage.install.permission=Erteilung der Ausführungsberechtigung für {} -appimage.missing_downloader=Kein Download-Client installiert ({clients}) appimage.update_database.deleting_old=Entfernen von alten Dateien appimage.update_database.downloading=Herunterladen von Datenbankdateien appimage.update_database.uncompressing=Dekomprimierung von Dateien diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index 487426a9a..609bd590f 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -33,6 +33,7 @@ appimage.info.icon_path=icon appimage.info.imported.false=No appimage.info.imported.true=Yes appimage.info.install_dir=Installation directory +appimage.info.repository=Repository appimage.info.symlink=Symlink appimage.info.url_download=File URL appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Generating a menu shortcut appimage.install.extract=Extracting the content from {} appimage.install.imported.rename_error=It was not possible to move the file {} to {} appimage.install.permission=Giving execution permission to {} -appimage.missing_downloader=No download client installed ({clients}) appimage.update_database.deleting_old=Removing old files appimage.update_database.downloading=Downloading database files appimage.update_database.uncompressing=Uncompressing files diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index e98a0ccb1..b8b038daa 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -33,6 +33,7 @@ appimage.info.icon_path=icono appimage.info.imported.false=No appimage.info.imported.true=Sí appimage.info.install_dir=Directorio de instalación +appimage.info.repository=Repositorio appimage.info.symlink=Link simbólico appimage.info.url_download=URL del archivo appimage.install.appimagelauncher.error={appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Creando un atajo en el menú appimage.install.extract=Extrayendo el contenido de {} appimage.install.imported.rename_error=No fue posible mover el archivo {} a {} appimage.install.permission=Concediendo permiso de ejecución a {} -appimage.missing_downloader=No hay ningún cliente de descarga instalado ({clients}) appimage.update_database.deleting_old=Eliminando archivos antiguos appimage.update_database.downloading=Descargando archivos de la base de datos appimage.update_database.uncompressing=Descomprindo archivos diff --git a/bauh/gems/appimage/resources/locale/fr b/bauh/gems/appimage/resources/locale/fr index 996bf8f9f..7019c30ec 100644 --- a/bauh/gems/appimage/resources/locale/fr +++ b/bauh/gems/appimage/resources/locale/fr @@ -33,6 +33,7 @@ appimage.info.icon_path=icône appimage.info.imported.false=Non appimage.info.imported.true=Oui appimage.info.install_dir=Répertoire d'installation +appimage.info.repository=Dépôt appimage.info.symlink=Symlink appimage.info.url_download=URL du fichier appimage.install.appimagelauncher.error={appimgl} n'autorise pas {app} à être installé. Désinstallez {appimgl}, redemarrez, puis essayez d'installer {app} de nouveau. @@ -41,7 +42,6 @@ appimage.install.download.error=Échec du téléchargement du fichier {}. Le ser appimage.install.extract=Extractiion du contenu de {} appimage.install.imported.rename_error=Impossible de déplacer {} vers {} appimage.install.permission={} est maintenant exécutable -appimage.missing_downloader=No download client installed ({clients}) appimage.update_database.deleting_old=Removing old files appimage.update_database.downloading=Downloading database files appimage.update_database.uncompressing=Uncompressing files diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it index 9ce0ce39d..2900fc126 100644 --- a/bauh/gems/appimage/resources/locale/it +++ b/bauh/gems/appimage/resources/locale/it @@ -33,6 +33,7 @@ appimage.info.icon_path=icona appimage.info.imported.false=No appimage.info.imported.true=Yes appimage.info.install_dir=Installation directory +appimage.info.repository=Deposito appimage.info.symlink=Symlink appimage.info.url_download=File URL appimage.install.appimagelauncher.error={appimgl} non consente l'installazione di {app}. Disinstallare {appimgl}, riavviare il sistema e provare a installare nuovamente {app}. @@ -41,7 +42,6 @@ appimage.install.download.error=Non è stato possibile scaricare il file {}. Il appimage.install.extract=Estrarre il contenuto da {} appimage.install.imported.rename_error=It was not possible to move the file {} to {} appimage.install.permission=Gdare il permesso di esecuzione a {} -appimage.missing_downloader=No download client installed ({clients}) appimage.update_database.deleting_old=Removing old files appimage.update_database.downloading=Downloading database files appimage.update_database.uncompressing=Uncompressing files diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 572d8d05a..d421aacdd 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -33,6 +33,7 @@ appimage.info.icon_path=ícone appimage.info.imported.false=Não appimage.info.imported.true=Sim appimage.info.install_dir=Diretório de instalação +appimage.info.repository=Repositório appimage.info.symlink=Link simbólico appimage.info.url_download=URL do arquivo 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. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Criando um atalho no menu appimage.install.extract=Extraindo o conteúdo de {} appimage.install.imported.rename_error=Não foi possível mover o arquivo {} para {} appimage.install.permission=Concedendo permissão de execução para {} -appimage.missing_downloader=Nenhum cliente de para download está instalado ({clients}) appimage.update_database.deleting_old=Removendo arquicos antigos appimage.update_database.downloading=Baixando arquivos do banco de dados appimage.update_database.uncompressing=Descomprimindo arquivos diff --git a/bauh/gems/appimage/resources/locale/ru b/bauh/gems/appimage/resources/locale/ru index c48754fb5..f6fb3c99d 100644 --- a/bauh/gems/appimage/resources/locale/ru +++ b/bauh/gems/appimage/resources/locale/ru @@ -33,6 +33,7 @@ appimage.info.icon_path=Значок appimage.info.imported.false=Нет appimage.info.imported.true=Да appimage.info.install_dir=Директория установки +appimage.info.repository=Pепозиторий appimage.info.symlink=Символическая ссылка appimage.info.url_download=URL Файла appimage.install.appimagelauncher.error={appimgl} не позволяет установить {app} . Удалите {appimgl}, перезагрузите ваш компьютер и попробуйте снова установить {app}. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Создать ярлык в меню appimage.install.extract=Извлечь содержимое из {} appimage.install.imported.rename_error=Не удалось переместить файл {} в {} appimage.install.permission=Установить права на выполнение для {} -appimage.missing_downloader=Не установлен клиент загрузки ({clients}) appimage.update_database.deleting_old=Удаление старых файлов appimage.update_database.downloading=Загрузка файлов базы данных appimage.update_database.uncompressing=Распаковка файлов diff --git a/bauh/gems/appimage/resources/locale/tr b/bauh/gems/appimage/resources/locale/tr index a2b83e0fe..c3d54667d 100644 --- a/bauh/gems/appimage/resources/locale/tr +++ b/bauh/gems/appimage/resources/locale/tr @@ -33,6 +33,7 @@ appimage.info.icon_path=simge appimage.info.imported.false=Hayır appimage.info.imported.true=Evet appimage.info.install_dir=Kurulum dizini +appimage.info.repository=Depo appimage.info.symlink=Symlink appimage.info.url_download=Dosya Bağlantısı appimage.install.appimagelauncher.error={appimgl}, {app} uygulamasının yüklenmesine izin vermiyor. {appimgl} yazılımını kaldırın, sisteminizi yeniden başlatın ve {app} yazılımını tekrar yüklemeyi deneyin. @@ -40,7 +41,6 @@ appimage.install.desktop_entry=Bir menü kısayolu oluşturuluyor appimage.install.extract={} 'den içerik ayıklanıyor appimage.install.imported.rename_error={} dosyasını {} klasörüne taşımak mümkün değildi appimage.install.permission={} için yürütme izni veriliyor -appimage.missing_downloader=No download client installed ({clients}) appimage.update_database.deleting_old=Removing old files appimage.update_database.downloading=Downloading database files appimage.update_database.uncompressing=Uncompressing files diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 22c982b9c..02b43bba7 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -11,7 +11,7 @@ from pathlib import Path from pwd import getpwnam from threading import Thread -from typing import List, Set, Type, Tuple, Dict, Iterable, Optional, Collection, Generator +from typing import List, Set, Type, Tuple, Dict, Iterable, Optional, Collection, Generator, Any from dateutil.parser import parse as parse_date @@ -1332,7 +1332,7 @@ def _map_actual_replacers(self, names: Set[str], context: TransactionContext) -> thread_fill_aur = None if aur_replacers: - thread_fill_aur = Thread(target=self._fill_aur_providers, args=(aur_replacers, actual_replacers)) + thread_fill_aur = Thread(target=self._fill_aur_providers, args=(aur_replacers, actual_replacers), daemon=True) thread_fill_aur.start() if repo_replacers: @@ -1551,84 +1551,95 @@ def uninstall(self, pkg: ArchPackage, root_password: Optional[str], watcher: Pro def get_managed_types(self) -> Set["type"]: return {ArchPackage} - def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict: - fill_pkgbuild = Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True) - fill_pkgbuild.start() - - if pkg.installed: - info = pacman.get_info_dict(pkg.name) - - if info is not None: - self._parse_dates_string_from_info(pkg.name, info) - - info['04_orphan'] = pkg.orphan - info['04_out_of_date'] = pkg.out_of_date + def _map_info_aur_installed(self, pkg: ArchPackage, pkgbuild_thread: Thread) -> Dict[str, Any]: + info = pacman.get_info_dict(pkg.name) - if pkg.commit: - info['commit'] = pkg.commit + if info is not None: + self._parse_dates_string_from_info(pkg.name, info) - if pkg.last_modified: - info['last_modified'] = self._parse_timestamp(ts=pkg.last_modified, - error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified)) + info['04_orphan'] = pkg.orphan + info['04_out_of_date'] = pkg.out_of_date - info['14_installed_files'] = pacman.list_installed_files(pkg.name) + if pkg.commit: + info['commit'] = pkg.commit - fill_pkgbuild.join() + if pkg.last_modified: + err_msg = f"Could not parse AUR package '{pkg.name}' 'last_modified' field ({pkg.last_modified})" + info['last_modified'] = self._parse_timestamp(ts=pkg.last_modified, error_msg=err_msg) - if pkg.pkgbuild: - info['13_pkg_build'] = pkg.pkgbuild + info['14_installed_files'] = pacman.list_installed_files(pkg.name) - return info - else: - info = { - '01_id': pkg.id, - '02_name': pkg.name, - '03_description': pkg.description, - '03_version': pkg.version, - '04_orphan': pkg.orphan, - '04_out_of_date': pkg.out_of_date, - '04_popularity': pkg.popularity, - '05_votes': pkg.votes, - '06_package_base': pkg.package_base, - '07_maintainer': pkg.maintainer, - '10_url': pkg.url_download - } + pkgbuild_thread.join() - if pkg.first_submitted: - info['08_first_submitted'] = self._parse_timestamp(ts=pkg.first_submitted, - error_msg="Could not parse AUR package '{}' 'first_submitted' field ({})".format(pkg.name, pkg.first_submitted)) + if pkg.pkgbuild: + info['13_pkg_build'] = pkg.pkgbuild - if pkg.last_modified: - info['09_last_modified'] = self._parse_timestamp(ts=pkg.last_modified, - error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified)) + return info - srcinfo = self.aur_client.get_src_info(pkg.name) + def _map_info_aur_uninstalled(self, pkg: ArchPackage, pkgbuild_thread: Thread) -> Dict[str, Any]: + info = { + '01_id': pkg.id, + '02_name': pkg.name, + '03_description': pkg.description, + '03_version': pkg.version, + '04_orphan': pkg.orphan, + '04_out_of_date': pkg.out_of_date, + '04_popularity': pkg.popularity, + '05_votes': pkg.votes, + '06_package_base': pkg.package_base, + '07_maintainer': pkg.maintainer, + '10_url': pkg.url_download + } + + if pkg.first_submitted: + info['08_first_submitted'] = self._parse_timestamp(ts=pkg.first_submitted, + error_msg="Could not parse AUR package '{}' 'first_submitted' field ({})".format( + pkg.name, pkg.first_submitted)) + + if pkg.last_modified: + info['09_last_modified'] = self._parse_timestamp(ts=pkg.last_modified, + error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format( + pkg.name, pkg.last_modified)) + + srcinfo = self.aur_client.get_src_info(pkg.name) + + if srcinfo: + arch_str = 'x86_64' if self.context.is_system_x86_64() else 'i686' + for info_attr, src_attr in {'12_makedepends': 'makedepends', + '13_dependson': 'depends', + '14_optdepends': 'optdepends', + 'checkdepends': '15_checkdepends'}.items(): + if srcinfo.get(src_attr): + info[info_attr] = [*srcinfo[src_attr]] + + arch_attr = f"{src_attr}_{arch_str}" + + if srcinfo.get(arch_attr): + if not info.get(info_attr): + info[info_attr] = [*srcinfo[arch_attr]] + else: + info[info_attr].extend(srcinfo[arch_attr]) - if srcinfo: - arch_str = 'x86_64' if self.context.is_system_x86_64() else 'i686' - for info_attr, src_attr in {'12_makedepends': 'makedepends', - '13_dependson': 'depends', - '14_optdepends': 'optdepends', - 'checkdepends': '15_checkdepends'}.items(): - if srcinfo.get(src_attr): - info[info_attr] = [*srcinfo[src_attr]] + pkgbuild_thread.join() - arch_attr = '{}_{}'.format(src_attr, arch_str) + if pkg.pkgbuild: + info['00_pkg_build'] = pkg.pkgbuild + else: + info['11_pkg_build_url'] = pkg.get_pkg_build_url() - if srcinfo.get(arch_attr): - if not info.get(info_attr): - info[info_attr] = [*srcinfo[arch_attr]] - else: - info[info_attr].extend(srcinfo[arch_attr]) + return info - fill_pkgbuild.join() + def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict: + fill_pkgbuild = Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True) + fill_pkgbuild.start() - if pkg.pkgbuild: - info['00_pkg_build'] = pkg.pkgbuild - else: - info['11_pkg_build_url'] = pkg.get_pkg_build_url() + if pkg.installed: + info = self._map_info_aur_installed(pkg, fill_pkgbuild) + else: + info = self._map_info_aur_uninstalled(pkg, fill_pkgbuild) - return info + info["00_url"] = f"https://aur.archlinux.org/packages/{pkg.get_base_name()}" + return info def _parse_dates_string_from_info(self, pkgname: str, info: dict): for date_attr in ('install date', 'build date'): @@ -3763,15 +3774,15 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ name_priority = dict() - fill_suggestions = Thread(target=self._fill_suggestions, args=(name_priority,)) + fill_suggestions = Thread(target=self._fill_suggestions, args=(name_priority,), daemon=True) fill_suggestions.start() available_packages = dict() - fill_available = Thread(target=self._fill_available_packages, args=(available_packages,)) + fill_available = Thread(target=self._fill_available_packages, args=(available_packages,), daemon=True) fill_available.start() ignored_pkgs = set() - fill_ignored = Thread(target=pacman.fill_ignored_packages, args=(ignored_pkgs,)) + fill_ignored = Thread(target=pacman.fill_ignored_packages, args=(ignored_pkgs,), daemon=True) fill_ignored.start() fill_suggestions.join() @@ -3806,7 +3817,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ if filter_installed: ignored_updates = set() - thread_fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,)) + thread_fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,), daemon=True) thread_fill_ignored_updates.start() else: ignored_updates, thread_fill_ignored_updates = None, None @@ -3854,7 +3865,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ update_ignored=pkg_updates_ignored) if disk_loader: - t = Thread(target=self._fill_cached_if_unset, args=(pkg, disk_loader)) + t = Thread(target=self._fill_cached_if_unset, args=(pkg, disk_loader), daemon=True) t.start() caching_threads.append(t) diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index f0b2f2eae..7ae70db88 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -68,7 +68,7 @@ def get_missing_packages(self, names: Set[str], repository: Optional[str] = None if not repository: for name in missing_names: - t = Thread(target=self._fill_repository, args=(name, missing_root)) + t = Thread(target=self._fill_repository, args=(name, missing_root), daemon=True) t.start() threads.append(t) @@ -518,7 +518,8 @@ def _fill_single_providers_data(self, all_missing_deps: Iterable[Tuple[str, str] if aur_providers_no_data: aur_providers_data = dict() aur_data_filler = Thread(target=self._fill_aur_updates_data, - args=(aur_providers_no_data, aur_providers_data)) + args=(aur_providers_no_data, aur_providers_data), + daemon=True) aur_data_filler.start() if repo_providers_no_data: diff --git a/bauh/gems/arch/download.py b/bauh/gems/arch/download.py index 09513473b..e4e2d21bc 100644 --- a/bauh/gems/arch/download.py +++ b/bauh/gems/arch/download.py @@ -88,7 +88,9 @@ def download_package(self, pkg: Dict[str, str], root_password: Optional[str], su watcher.print("Could not download '{}' from mirror '{}'".format(pkgname, mirror)) else: self.logger.info("Package '{}' successfully downloaded".format(pkg['n'])) - t = Thread(target=self.download_package_signature, args=(pkg, url, output_path, root_password, watcher), daemon=True) + t = Thread(target=self.download_package_signature, + args=(pkg, url, output_path, root_password, watcher), + daemon=True) t.start() self.async_downloads_lock.acquire() self.async_downloads.append(t) diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index d7e368867..fd09525e8 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -288,3 +288,6 @@ def __hash__(self): @property def orphan(self) -> bool: return self.maintainer is None + + def is_trustable(self) -> bool: + return self.repository and self.repository != "aur" diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index d9572d691..00cef089e 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -146,15 +146,14 @@ def map_packages(names: Optional[Iterable[str]] = None, remote: bool = False, si if ignored: for key in ('signed', 'not_signed'): - to_del = set() - if pkgs.get(key): + to_del = set() for pkg in pkgs[key].keys(): if pkg in ignored: to_del.add(pkg) - for pkg in to_del: - del pkgs[key][pkg] + for pkg in to_del: + del pkgs[key][pkg] return pkgs diff --git a/bauh/gems/arch/resources/__init__.py b/bauh/gems/arch/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/arch/resources/img/__init__.py b/bauh/gems/arch/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/arch/resources/locale/__init__.py b/bauh/gems/arch/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index d1995e915..1cf570e4e 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -143,6 +143,7 @@ arch.downgrade.version_found=S’ha trobat la versió actual del paquet arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=PKGBUILD +arch.info.00_url=URL arch.info.01_id=identificació arch.info.02_name=nom arch.info.03_description=descripció @@ -313,4 +314,3 @@ gem.arch.info=Software packages available for distributions based on Arch Linux gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Dipòsit gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=Els paquets AUR són mantinguts per una comunitat d’usuaris independent. No hi ha cap garantia que funcionin o que no danyin el vostre sistema. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 1c0c9f96f..74bbc6e87 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -143,6 +143,7 @@ arch.downgrade.version_found=Aktuelle Paketversion gefunden arch.aur.error.missing_root_dep={dep} ist nicht installiert und wird für die Installation von {aur} Paketen als {root} Benutzer benötigt arch.aur.error.add_builder_user=Es war nicht möglich, den Benutzer {user} für die Erzeugung von {aur}-Paketen zu erstellen arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=ID arch.info.02_name=Name arch.info.03_description=Beschreibung @@ -313,4 +314,3 @@ gem.arch.info=Verfügbare Softwarepakete für Distributionen, die auf Arch Linux gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Repository gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=AUR-Pakete werden von einer unabhängigen Benutzergemeinschaft gepflegt. Es gibt keine Garantie, dass sie funktionieren oder Ihr System nicht beschädigen. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index d68de2e81..e75fd066e 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -143,6 +143,7 @@ arch.downgrade.version_found=Current package version found arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=id arch.info.02_name=name arch.info.03_description=description @@ -313,4 +314,3 @@ gem.arch.info=Software packages available for distributions based on Arch Linux gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Repository gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=AUR packages are maintained by an independent user community. There is no warranty they will work or not harm your system. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 4a9faebdb..7cdb770f5 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -143,6 +143,7 @@ arch.downgrade.version_found=Version actual del paquete encontrada arch.aur.error.missing_root_dep={dep} no está instalado y es necesario para la instalación de paquetes del {aur} como el usuario {root} arch.aur.error.add_builder_user=No fue posible crear el usuario {user} para construir paquetes del {aur} arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=id arch.info.02_name=nombre arch.info.03_description=descripción @@ -313,4 +314,3 @@ gem.arch.info=Paquetes de software disponibles para distribuciones basadas en Ar gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Repositorio gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=Los paquetes AUR son mantenidos por una comunidad de usuarios independiente. No hay garantía de que funcionen o que no dañen su sistema. diff --git a/bauh/gems/arch/resources/locale/fr b/bauh/gems/arch/resources/locale/fr index d9e513428..47e819e50 100644 --- a/bauh/gems/arch/resources/locale/fr +++ b/bauh/gems/arch/resources/locale/fr @@ -143,6 +143,7 @@ arch.downgrade.version_found=Version actuelle du paquet trouvée arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=id arch.info.02_name=nom arch.info.03_description=description @@ -313,4 +314,3 @@ gem.arch.info=Paquets logiciels disponibles pour les distributions basées sur A gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Dépôt gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=Les paquets AUR sont maintenus par une communauté indépendante. Il n'y a aucune garantie qu'ils fonctionneront et qu'ils n'endommageront pas votre système. diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 7dd9e9f79..6b05004d3 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -143,6 +143,7 @@ arch.downgrade.version_found=Trovata la versione del pacchetto corrente arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=id arch.info.02_name=nome arch.info.03_description=descrizione @@ -313,4 +314,3 @@ gem.arch.info=Software packages available for distributions based on Arch Linux gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Deposito gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=I pacchetti AUR sono gestiti da una comunità di utenti indipendenti. Non esiste alcuna garanzia che funzionino o non danneggino il sistema. \ No newline at end of file diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 9823ae760..6c0512726 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -142,6 +142,7 @@ arch.downgrade.version_found=Versão atual do pacote encontrada arch.aur.error.missing_root_dep={dep} não está instalado e é necessário para a instalação de pacotes do {aur} como o usuário {root} arch.aur.error.add_builder_user=Não foi possível criar o usuário {user} para a construção de pacotes do {aur} arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=id arch.info.02_name=nome arch.info.03_description=descrição @@ -312,4 +313,3 @@ gem.arch.info=Pacotes de software disponíveis para distribuições baseadas em gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Repositório gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=Pacotes do AUR são mantidos por uma comunidade de usuários independente. Não há garantia que funcionarão ou que não prejudicarão o seus sistema. diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index ba5d74489..c6cd95dde 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -143,6 +143,7 @@ arch.downgrade.version_found=Найдена текущая версия паке arch.aur.error.missing_root_dep={dep} не установлен и необходим для установки пакетов {aur} от имени пользователя {root} arch.aur.error.add_builder_user=Не удалось создать пользователя {user} для сборки пакетов {aur}. arch.info.00_pkg_build=PKGBUILD +arch.info.00_url=URL arch.info.01_id=Идентификатор arch.info.02_name=Имя arch.info.03_description=Описание @@ -313,4 +314,3 @@ gem.arch.info=Пакеты программного обеспечения, до gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Репозиторий gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=AUR-пакеты поддерживаются независимым сообществом пользователей. Нет никакой гарантии, что они будут работать или не навредят вашей системе. diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 9a9431283..8aeb0f48d 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -143,6 +143,7 @@ arch.downgrade.version_found=Geçerli paket sürümü bulundu arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild +arch.info.00_url=URL arch.info.01_id=kimlik arch.info.02_name=isim arch.info.03_description=açıklama @@ -313,4 +314,3 @@ gem.arch.info=Arch Linux tabanlı dağıtımlar için mevcut yazılım paketleri gem.arch.label=Arch gem.arch.type.arch_repo.label=Arch - Depoları gem.arch.type.aur.label=Arch - AUR -gem.aur.install.warning=AUR paketleri bağımsız bir Arch Kullanıcı Topluluğu tarafından sağlanır. Çalışacaklarının veya sisteminize zarar vermeyeceklerinin garantisi yoktur. diff --git a/bauh/gems/debian/controller.py b/bauh/gems/debian/controller.py index 6708aeb14..5f1182a5c 100644 --- a/bauh/gems/debian/controller.py +++ b/bauh/gems/debian/controller.py @@ -60,7 +60,7 @@ def _update_apps_index(self, apps: Iterable[DebianApplication]): def search(self, words: str, disk_loader: Optional[DiskCacheLoader], limit: int, is_url: bool) -> SearchResult: config_ = dict() - fill_config = Thread(target=self._fill_config, args=(config_,)) + fill_config = Thread(target=self._fill_config, args=(config_,), daemon=True) fill_config.start() res = SearchResult.empty() @@ -102,11 +102,11 @@ def read_installed(self, disk_loader: Optional[DiskCacheLoader], pkg_types: Opti names: Optional[Iterable[str]] = None) -> SearchResult: config_ = dict() - fill_config = Thread(target=self._fill_config, args=(config_,)) + fill_config = Thread(target=self._fill_config, args=(config_,), daemon=True) fill_config.start() ignored_updates = set() - fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,)) + fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,), daemon=True) fill_ignored_updates.start() threads = (fill_config, fill_ignored_updates) @@ -170,7 +170,7 @@ def uninstall(self, pkg: DebianPackage, root_password: str, watcher: ProcessWatc if deps: # updates are required to be filled in case the dependencies are currently displayed on the view updates = dict() - fill_updates = Thread(target=self._fill_updates, args=(updates,)) + fill_updates = Thread(target=self._fill_updates, args=(updates,), daemon=True) fill_updates.start() deps_data = self.aptitude.show((p.name for p in deps), attrs=('description', 'maintainer', 'section')) @@ -517,7 +517,7 @@ def prepare(self, task_manager: Optional[TaskManager], root_password: Optional[s def list_updates(self, internet_available: bool) -> List[PackageUpdate]: ignored_updates = set() - fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,)) + fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,), daemon=True) fill_ignored_updates.start() updates = list() @@ -551,7 +551,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ if filter_installed: installed = set() - fill_installed = Thread(target=self._fill_installed_names, args=(installed, )) + fill_installed = Thread(target=self._fill_installed_names, args=(installed,), daemon=True) fill_installed.start() else: installed, fill_installed = None, None diff --git a/bauh/gems/debian/model.py b/bauh/gems/debian/model.py index a41888433..e506a373b 100644 --- a/bauh/gems/debian/model.py +++ b/bauh/gems/debian/model.py @@ -132,6 +132,9 @@ def is_update_ignored(self) -> bool: def supports_ignored_updates(self) -> bool: return True + def is_trustable(self) -> bool: + return True + def __eq__(self, other): if isinstance(other, DebianPackage): return self.name == other.name diff --git a/bauh/gems/debian/resources/__init__.py b/bauh/gems/debian/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/debian/resources/img/__init__.py b/bauh/gems/debian/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/debian/resources/locale/__init__.py b/bauh/gems/debian/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index e1258c591..ac9bdebb7 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -135,7 +135,7 @@ def _fill_required_runtime_updates(self, output: Dict[str, List[Tuple[str, str]] for installation in ('system', 'user'): runtimes = list() output[installation] = runtimes - t = Thread(target=self._fill_required_runtimes, args=(installation, runtimes)) + t = Thread(target=self._fill_required_runtimes, args=(installation, runtimes), daemon=True) t.start() threads.append(t) @@ -150,11 +150,13 @@ def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1 thread_updates, thread_runtimes = None, None if internet_available: - thread_updates = Thread(target=self._add_updates, args=(version, updates)) + thread_updates = Thread(target=self._add_updates, args=(version, updates), daemon=True) thread_updates.start() if version >= VERSION_1_12: - thread_runtimes = Thread(target=self._fill_required_runtime_updates, args=(required_runtimes,)) + thread_runtimes = Thread(target=self._fill_required_runtime_updates, + args=(required_runtimes,), + daemon=True) thread_runtimes.start() installed = flatpak.list_installed(version) @@ -665,7 +667,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ cached_count += 1 else: fill = Thread(target=self._fill_suggestion, args=(appid, ids_prios[appid], flatpak_version, - remote, res)) + remote, res), daemon=True) fill.start() fill_suggestions.append(fill) diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index dbc4d133b..bbb851135 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -195,7 +195,7 @@ def list_updates_as_str(version: Tuple[str, ...]) -> Dict[str, Set[str]]: threads = [] for type_, output in (('system', sys_updates), ('user', user_updates)): - fill = Thread(target=fill_updates, args=(version, type_, output)) + fill = Thread(target=fill_updates, args=(version, type_, output), daemon=True) fill.start() threads.append(fill) diff --git a/bauh/gems/flatpak/model.py b/bauh/gems/flatpak/model.py index c44ff456b..381386e2f 100644 --- a/bauh/gems/flatpak/model.py +++ b/bauh/gems/flatpak/model.py @@ -159,6 +159,9 @@ def update_ref(self): if self.id and self.arch and self.branch: self.ref = f'{self.id}/{self.arch}/{self.branch}' + def is_trustable(self) -> bool: + return False + def __repr__(self) -> str: return f'Flatpak (id={self.id}, branch={self.branch}, origin={self.origin}, installation={self.installation},' \ f' partial={self.partial}, update_component={self.update_component})' diff --git a/bauh/gems/flatpak/resources/__init__.py b/bauh/gems/flatpak/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/flatpak/resources/img/__init__.py b/bauh/gems/flatpak/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/flatpak/resources/locale/__init__.py b/bauh/gems/flatpak/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 03468e2a1..beb07b677 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -434,7 +434,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ res.append(cached_sug) cached_count += 1 else: - t = Thread(target=self._fill_suggestion, args=(name, ids_prios[name], snapd_client, res)) + t = Thread(target=self._fill_suggestion, args=(name, ids_prios[name], snapd_client, res), daemon=True) t.start() threads.append(t) time.sleep(0.001) # to avoid being blocked diff --git a/bauh/gems/snap/resources/__init__.py b/bauh/gems/snap/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/snap/resources/img/__init__.py b/bauh/gems/snap/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/snap/resources/locale/__init__.py b/bauh/gems/snap/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 02ab6f1f2..7e7d8ca35 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -253,12 +253,12 @@ def _request_url(self, url: str) -> Optional[Response]: def _map_url(self, url: str) -> Tuple["BeautifulSoup", requests.Response]: url_res = self._request_url(url) - if url_res: + if url_res is not None and url_res.status_code != 404: return BeautifulSoup(url_res.text, 'lxml', parse_only=SoupStrainer('head')), url_res def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: web_config = {} - thread_config = Thread(target=self._fill_config_async, args=(web_config,)) + thread_config = Thread(target=self._fill_config_async, args=(web_config,), daemon=True) thread_config.start() res = SearchResult([], [], 0) @@ -1069,7 +1069,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> Optional[List[ web_config = {} - thread_config = Thread(target=self._fill_config_async, args=(web_config,)) + thread_config = Thread(target=self._fill_config_async, args=(web_config,), daemon=True) thread_config.start() if self.suggestions: diff --git a/bauh/gems/web/environment.py b/bauh/gems/web/environment.py index d32e004be..37c59e08f 100644 --- a/bauh/gems/web/environment.py +++ b/bauh/gems/web/environment.py @@ -421,11 +421,13 @@ def check_environment(self, env: dict, local_config: dict, app: WebApplication, if system_env: self.logger.warning(f"Using system's nativefier to install {app.url}") else: - node_check = Thread(target=self._check_and_fill_node, args=(env, components)) + node_check = Thread(target=self._check_and_fill_node, args=(env, components), daemon=True) node_check.start() check_threads.append(node_check) - elec_check = Thread(target=self._check_and_fill_electron, args=(app, env, local_config, is_x86_x64_arch, widevine, components)) + elec_check = Thread(target=self._check_and_fill_electron, + args=(app, env, local_config, is_x86_x64_arch, widevine, components), + daemon=True) elec_check.start() check_threads.append(elec_check) diff --git a/bauh/gems/web/resources/__init__.py b/bauh/gems/web/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/web/resources/img/__init__.py b/bauh/gems/web/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/gems/web/resources/locale/__init__.py b/bauh/gems/web/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/manage.py b/bauh/manage.py index 495148b34..469aefb5e 100644 --- a/bauh/manage.py +++ b/bauh/manage.py @@ -18,16 +18,14 @@ from bauh.view.qt.settings import SettingsWindow from bauh.view.qt.window import ManageWindow from bauh.view.util import resource, util -from bauh.view.util.cache import CacheCleaner, DefaultMemoryCacheFactory +from bauh.view.util.cache import DefaultMemoryCacheFactory from bauh.view.util.disk import DefaultDiskCacheLoaderFactory def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logger) -> Tuple[QApplication, QWidget]: i18n = generate_i18n(app_config, resource.get_path('locale')) - cache_cleaner = CacheCleaner() - - cache_factory = DefaultMemoryCacheFactory(expiration_time=int(app_config['memory_cache']['data_expiration']), cleaner=cache_cleaner) + cache_factory = DefaultMemoryCacheFactory(expiration_time=int(app_config['memory_cache']['data_expiration'])) icon_cache = cache_factory.new(int(app_config['memory_cache']['icon_expiration'])) http_client = HttpClient(logger) @@ -83,12 +81,9 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg force_suggestions=force_suggestions, logger=logger) - prepare = PreparePanel(context=context, - manager=manager, - i18n=i18n, - manage_window=manage_window, - app_config=app_config, - force_suggestions=force_suggestions) - cache_cleaner.start() - - return app, prepare + return app, PreparePanel(context=context, + manager=manager, + i18n=i18n, + manage_window=manage_window, + app_config=app_config, + force_suggestions=force_suggestions) diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index f1987914c..4da5652b2 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -1,11 +1,9 @@ -import os -import re import shutil import time import traceback from subprocess import Popen, STDOUT from threading import Thread -from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable, Pattern +from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ UpgradeRequirement, TransactionResult, SoftwareAction, SettingsView, SettingsController @@ -13,10 +11,11 @@ from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \ CustomSoftwareAction -from bauh.api.abstract.view import ViewComponent, TabGroupComponent, MessageType, PanelComponent +from bauh.api.abstract.view import TabGroupComponent, MessageType from bauh.api.exception import NoInternetException from bauh.commons.boot import CreateConfigFile from bauh.commons.html import bold +from bauh.commons.regex import RE_URL from bauh.commons.util import sanitize_command_input from bauh.view.core.config import CoreConfigManager from bauh.view.core.settings import GenericSettingsManager @@ -25,8 +24,6 @@ from bauh.view.util.resource import get_path from bauh.view.util.util import clean_app_files, restart_app -RE_IS_URL = re.compile(r'^https?://.+') - class GenericUpgradeRequirements(UpgradeRequirements): @@ -144,14 +141,13 @@ def _can_work(self, man: SoftwareManager): return available def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult): - if self._can_work(man): - mti = time.time() - apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds') + mti = time.time() + apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds') - res.installed.extend(apps_found.installed) - res.new.extend(apps_found.new) + res.installed.extend(apps_found.installed) + res.new.extend(apps_found.new) def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult: ti = time.time() @@ -164,16 +160,17 @@ def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = - self.logger.info(f"Search query: {norm_query}") if norm_query: - is_url = bool(RE_IS_URL.match(norm_query)) + is_url = bool(RE_URL.match(norm_query)) disk_loader = self.disk_loader_factory.new() disk_loader.start() threads = [] for man in self.managers: - t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res)) - t.start() - threads.append(t) + if self._can_work(man): + t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res), daemon=True) + t.start() + threads.append(t) for t in threads: t.join() @@ -181,9 +178,6 @@ def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = - if disk_loader: disk_loader.stop_working() disk_loader.join() - - # res.installed = self._sort(res.installed, norm_word) - # res.new = self._sort(res.new, norm_word) else: raise NoInternetException() @@ -206,6 +200,15 @@ def can_work(self) -> Tuple[bool, Optional[str]]: def _get_package_lower_name(self, pkg: SoftwarePackage): return pkg.name.lower() + def _fill_read_installed(self, man: SoftwareManager, disk_loader: DiskCacheLoader, internet_available: bool, + output: List[SearchResult]): + mti = time.time() + man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available, + limit=-1, only_apps=False) + mtf = time.time() + self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.4f} seconds') + output.append(man_res) + def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: ti = time.time() self._wait_to_be_ready() @@ -215,6 +218,9 @@ def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, o disk_loader = None net_available = self.context.is_internet_available() + read_threads = list() + results = list() + if not pkg_types: # any type for man in self.managers: if self._can_work(man): @@ -222,13 +228,11 @@ def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, o disk_loader = self.disk_loader_factory.new() disk_loader.start() - mti = time.time() - man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_available) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds') - - res.installed.extend(man_res.installed) - res.total += man_res.total + t = Thread(target=self._fill_read_installed, + args=(man, disk_loader, net_available, results), + daemon=True) + t.start() + read_threads.append(t) else: man_already_used = [] @@ -240,18 +244,24 @@ def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, o disk_loader = self.disk_loader_factory.new() disk_loader.start() - mti = time.time() - man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_available) - mtf = time.time() - self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds') + t = Thread(target=self._fill_read_installed, + args=(man, disk_loader, net_available, results), + daemon=True) + t.start() + read_threads.append(t) - res.installed.extend(man_res.installed) - res.total += man_res.total + for t in read_threads: + t.join() if disk_loader: disk_loader.stop_working() disk_loader.join() + for result in results: + if result.installed: + res.installed.extend(result.installed) + res.total += result.total + if res.installed: for p in res.installed: if p.is_update_ignored(): @@ -498,7 +508,8 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSu suggestions, threads = [], [] for man in self.managers: t = Thread(target=self._fill_suggestions, - args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed)) + args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed), + daemon=True) t.start() threads.append(t) diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py index 313fd865c..982d12e4e 100644 --- a/bauh/view/core/downloader.py +++ b/bauh/view/core/downloader.py @@ -1,14 +1,14 @@ -import logging import os import re import shutil import time import traceback -from io import StringIO +from io import StringIO, BytesIO +from logging import Logger from math import floor from pathlib import Path from threading import Thread -from typing import Iterable, List, Optional +from typing import Optional, Tuple from bauh.api.abstract.download import FileDownloader from bauh.api.abstract.handler import ProcessWatcher @@ -21,17 +21,97 @@ RE_HAS_EXTENSION = re.compile(r'.+\.\w+$') +class SelfFileDownloader(FileDownloader): + + def __init__(self, logger: Logger, i18n: I18n, http_client: HttpClient, + check_ssl: bool): + self._logger = logger + self._i18n = i18n + self._client = http_client + self._ssl = check_ssl + + def is_multithreaded(self) -> bool: + return False + + def can_work(self) -> bool: + return True + + def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple() + + def is_multithreaded_client_available(self, name: str) -> bool: + return False + + def list_available_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple() + + def get_supported_clients(self) -> Tuple[str, ...]: + return tuple() + + def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str, + root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, + max_threads: int = None, known_size: int = None) -> bool: + try: + res = self._client.get(url=file_url, ignore_ssl=not self._ssl, stream=True) + except Exception: + return False + + try: + content_length = int(res.headers.get("content-length", 0)) + except Exception: + content_length = 0 + self._logger.warning(f"Could not retrieve the content-length for file '{file_url}'") + + file_name = file_url.split("/")[-1] + msg = StringIO() + msg.write(f"{substatus_prefix} " if substatus_prefix else "") + msg.write(f"{self._i18n['downloading']} {bold(file_name)}") + base_msg = msg.getvalue() + + byte_stream = BytesIO() + total_downloaded = 0 + known_size = content_length and content_length > 0 + total_size_str = get_human_size_str(content_length) if known_size > 0 else "?" + + try: + for data in res.iter_content(chunk_size=1024): + byte_stream.write(data) + total_downloaded += len(data) + perc = f"({(total_downloaded / content_length) * 100:.2f}%) " if known_size > 0 else "" + watcher.change_substatus(f"{perc}{base_msg} ({get_human_size_str(total_downloaded)} / {total_size_str})") + except Exception: + self._logger.error(f"Unexpected exception while downloading file from '{file_url}'") + traceback.print_exc() + return False + + self._logger.info(f"Writing downloaded file content to disk: {output_path}") + + try: + with open(output_path, "wb+") as f: + f.write(byte_stream.getvalue()) + except Exception: + self._logger.error(f"Unexpected exception when saving downloaded content to disk: {output_path}") + traceback.print_exc() + return False + + return True + + class AdaptableFileDownloader(FileDownloader): - def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient, + def __init__(self, logger: Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient, multithread_client: str, check_ssl: bool): self.logger = logger self.multithread_enabled = multithread_enabled self.i18n = i18n self.http_client = http_client - self.supported_multithread_clients = ['aria2', 'axel'] + self.supported_multithread_clients = ("aria2", "axel") self.multithread_client = multithread_client self.check_ssl = check_ssl + self._self_downloader = SelfFileDownloader(logger=logger, + i18n=i18n, + http_client=http_client, + check_ssl=check_ssl) @staticmethod def is_aria2c_available() -> bool: @@ -41,10 +121,6 @@ def is_aria2c_available() -> bool: def is_axel_available() -> bool: return bool(shutil.which('axel')) - @staticmethod - def is_wget_available() -> bool: - return bool(shutil.which('wget')) - def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess: cmd = ['aria2c', url, '--no-conf', @@ -85,18 +161,6 @@ def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password) - def _get_wget_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str]) -> SimpleProcess: - cmd = ['wget', url, '-c', '--retry-connrefused', '-t', '10', '-nc'] - - if not self.check_ssl: - cmd.append('--no-check-certificate') - - if output_path: - cmd.append('-O') - cmd.append(output_path) - - return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password) - def _rm_bad_file(self, file_name: str, output_path: str, cwd, handler: ProcessHandler, root_password: Optional[str]): to_delete = output_path if output_path else f'{cwd}/{file_name}' @@ -130,6 +194,45 @@ def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> return threads + def _download_with_threads(self, client: str, file_url: str, output_path: str, cwd: str, + max_threads: int, known_size: int, display_file_size: bool, handler: ProcessHandler, + root_password: Optional[str] = None, substatus_prefix: Optional[str] = None) \ + -> Tuple[float, bool]: + + threads = self._get_appropriate_threads_number(max_threads, known_size) + + if client == 'aria2': + start_time = time.time() + process = self._get_aria2c_process(file_url, output_path, cwd, root_password, threads) + downloader = 'aria2' + else: + start_time = time.time() + process = self._get_axel_process(file_url, output_path, cwd, root_password, threads) + downloader = 'axel' + + 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] + + if handler.watcher: + msg = StringIO() + msg.write(f'{substatus_prefix} ' if substatus_prefix else '') + msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}") + + if display_file_size: + if known_size: + msg.write(f' ( {get_human_size_str(known_size)} )') + handler.watcher.change_substatus(msg.getvalue()) + else: + Thread(target=self._concat_file_size, args=(file_url, msg, handler.watcher), daemon=True).start() + else: + msg.write(' ( ? Mb )') + handler.watcher.change_substatus(msg.getvalue()) + + success, _ = handler.handle_simple(process) + return start_time, success + def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool: self.logger.info(f'Downloading {file_url}') handler = ProcessHandler(watcher) @@ -138,7 +241,7 @@ def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = No final_cwd = cwd if cwd else '.' success = False - ti = time.time() + start_time = time.time() try: if output_path: if os.path.exists(output_path): @@ -155,50 +258,27 @@ def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = No watcher.print(self.i18n['error.mkdir'].format(dir=output_dir)) return False - client = self.get_available_multithreaded_tool() - if client: - threads = self._get_appropriate_threads_number(max_threads, known_size) - - if client == 'aria2': - ti = time.time() - process = self._get_aria2c_process(file_url, output_path, final_cwd, root_password, threads) - downloader = 'aria2' - else: - ti = time.time() - process = self._get_axel_process(file_url, output_path, final_cwd, root_password, threads) - downloader = 'axel' + threaded_client = self.get_available_multithreaded_tool() + if threaded_client: + start_time, success = self._download_with_threads(client=threaded_client, file_url=file_url, + output_path=output_path, + cwd=final_cwd, max_threads=max_threads, + known_size=known_size, handler=handler, + display_file_size=display_file_size, + root_password=root_password) else: - ti = time.time() - process = self._get_wget_process(file_url, output_path, final_cwd, root_password) - downloader = 'wget' - - 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] - - if watcher: - msg = StringIO() - msg.write(f'{substatus_prefix} ' if substatus_prefix else '') - msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}") - - if display_file_size: - if known_size: - msg.write(f' ( {get_human_size_str(known_size)} )') - watcher.change_substatus(msg.getvalue()) - else: - Thread(target=self._concat_file_size, args=(file_url, msg, watcher)).start() - else: - msg.write(' ( ? Mb )') - watcher.change_substatus(msg.getvalue()) - - success, _ = handler.handle_simple(process) + start_time = time.time() + success = self._self_downloader.download(file_url=file_url, watcher=watcher, output_path=output_path, + cwd=cwd, root_password=root_password, + substatus_prefix=substatus_prefix, + display_file_size=display_file_size, max_threads=max_threads, + known_size=known_size) except Exception: traceback.print_exc() self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password) - tf = time.time() - self.logger.info(f'{file_name} download took {(tf - ti) / 60:.2f} minutes') + final_time = time.time() + self.logger.info(f'{file_name} download took {(final_time - start_time) / 60:.4f} minutes') if not success: self.logger.error(f"Could not download '{file_name}'") @@ -228,9 +308,9 @@ def get_available_multithreaded_tool(self) -> str: return client def can_work(self) -> bool: - return self.is_wget_available() or self.is_multithreaded() + return True - def get_supported_multithreaded_clients(self) -> Iterable[str]: + def get_supported_multithreaded_clients(self) -> Tuple[str, ...]: return self.supported_multithread_clients def is_multithreaded_client_available(self, name: str) -> bool: @@ -241,8 +321,8 @@ def is_multithreaded_client_available(self, name: str) -> bool: else: return False - def list_available_multithreaded_clients(self) -> List[str]: - return [c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c)] + def list_available_multithreaded_clients(self) -> Tuple[str, ...]: + return tuple(c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c)) - def get_supported_clients(self) -> tuple: - return 'wget', 'aria2', 'axel' + def get_supported_clients(self) -> Tuple[str, ...]: + return "self", "aria2", "axel" diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py index dc1b69f53..ae276e566 100644 --- a/bauh/view/core/settings.py +++ b/bauh/view/core/settings.py @@ -146,7 +146,7 @@ def _gen_adv_settings(self, core_config: dict) -> TabComponent: return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), panel, None, 'core.adv') def _gen_multithread_client_select(self, core_config: dict) -> SingleSelectComponent: - available_mthread_clients = self.file_downloader.list_available_multithreaded_clients() + available_mthread_clients = [*self.file_downloader.list_available_multithreaded_clients()] available_mthread_clients.sort() default_i18n_key = 'default' if available_mthread_clients else 'core.config.download.multithreaded_client.none' @@ -533,14 +533,14 @@ def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[Li ti = time.time() save_threads, warnings, success_list = [], [], [] - save_core = Thread(target=self._save_core_settings, args=(component, success_list, warnings)) + save_core = Thread(target=self._save_core_settings, args=(component, success_list, warnings), daemon=True) save_core.start() save_threads.append(save_core) if self._settings_views: for views in self._settings_views.values(): - save_view = Thread(target=self._save_views, args=(views, success_list, warnings)) + save_view = Thread(target=self._save_views, args=(views, success_list, warnings), daemon=True) save_view.start() save_threads.append(save_view) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 29c2922d4..3fe9a5834 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -1,21 +1,24 @@ import operator import os from functools import reduce +from logging import Logger from threading import Lock -from typing import List, Optional +from typing import List, Optional, Dict -from PyQt5.QtCore import Qt, QUrl, QSize +from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QPixmap, QIcon, QCursor -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QToolButton, QWidget, \ QHeaderView, QLabel, QHBoxLayout, QToolBar, QSizePolicy from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.model import PackageStatus, CustomSoftwareAction +from bauh.api.abstract.view import MessageType from bauh.commons.html import strip_html, bold +from bauh.commons.regex import RE_URL from bauh.view.qt.components import IconButton, QCustomMenuAction, QCustomToolbar from bauh.view.qt.dialog import ConfirmationDialog from bauh.view.qt.qt_utils import get_current_screen_geometry +from bauh.view.qt.thread import URLFileDownloader from bauh.view.qt.view_model import PackageView from bauh.view.util.translation import I18n @@ -69,12 +72,13 @@ class PackagesTable(QTableWidget): COL_NUMBER = 9 DEFAULT_ICON_SIZE = QSize(16, 16) - def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool): + def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool, logger: Logger): super(PackagesTable, self).__init__() self.setObjectName('table_packages') self.setParent(parent) self.window = parent self.download_icons = download_icons + self.logger = logger self.setColumnCount(self.COL_NUMBER) self.setFocusPolicy(Qt.NoFocus) self.setShowGrid(False) @@ -88,13 +92,13 @@ def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: boo self.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) self.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor)) - self.network_man = QNetworkAccessManager() - self.network_man.finished.connect(self._load_icon_and_cache) + self.file_downloader: Optional[URLFileDownloader] = None self.icon_cache = icon_cache self.lock_async_data = Lock() self.setRowHeight(80, 80) self.cache_type_icon = {} + self.cache_default_icon: Dict[str, QIcon] = dict() self.i18n = self.window.i18n def has_any_settings(self, pkg: PackageView): @@ -183,10 +187,9 @@ def refresh(self, pkg: PackageView): self._update_row(pkg, screen_width, update_check_enabled=False, change_update_col=False) def update_package(self, pkg: PackageView, screen_width: int, change_update_col: bool = False): - if self.download_icons and pkg.model.icon_url: - icon_request = QNetworkRequest(QUrl(pkg.model.icon_url)) - icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) - self.network_man.get(icon_request) + if self.download_icons and pkg.model.icon_url and pkg.model.icon_url.startswith("http"): + self._setup_file_downloader(max_workers=1, max_downloads=1) + self.file_downloader.get(pkg.model.icon_url, pkg.table_index) self._update_row(pkg, screen_width, change_update_col=change_update_col) @@ -208,25 +211,28 @@ def _install_app(self, pkgv: PackageView): body = self.i18n['manage_window.apps_table.row.actions.install.popup.body'].format(self._bold(str(pkgv))) - warning = self.i18n.get('gem.{}.install.warning'.format(pkgv.model.get_type().lower())) - - if warning: + confirm_icon = MessageType.INFO + if not pkgv.model.is_trustable(): + warning = self.i18n["action.install.unverified.warning"] + confirm_icon = MessageType.WARNING body += '

{}'.format( '
'.join(('{}.'.format(phrase) for phrase in warning.split('.') if phrase))) if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.install.popup.title'], body=self._parag(body), - i18n=self.i18n).ask(): + i18n=self.i18n, + confirmation_icon_type=confirm_icon).ask(): self.window.install(pkgv) - def _load_icon_and_cache(self, http_response: QNetworkReply): - icon_url = http_response.request().url().toString() + def _update_pkg_icon(self, url_: str, content: Optional[bytes], table_idx: int): + if not content: + return content - icon_data = self.icon_cache.get(icon_url) + icon_data = self.icon_cache.get(url_) icon_was_cached = True if not icon_data: - icon_bytes = http_response.readAll() + icon_bytes = content if not icon_bytes: return @@ -238,16 +244,16 @@ def _load_icon_and_cache(self, http_response: QNetworkReply): if not pixmap.isNull(): icon = QIcon(pixmap) icon_data = {'icon': icon, 'bytes': icon_bytes} - self.icon_cache.add(icon_url, icon_data) + self.icon_cache.add(url_, icon_data) if icon_data: - for idx, app in enumerate(self.window.pkgs): - if app.model.icon_url == icon_url: - self._update_icon(self.cellWidget(idx, 0), icon_data['icon']) + for pkg in self.window.pkgs: + if pkg.table_index == table_idx: + self._update_icon(self.cellWidget(table_idx, 0), icon_data['icon']) - if app.model.supports_disk_cache() and app.model.get_disk_icon_path() and icon_data['bytes']: - if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()): - self.window.manager.cache_to_disk(pkg=app.model, icon_bytes=icon_data['bytes'], + if pkg.model.supports_disk_cache() and pkg.model.get_disk_icon_path() and icon_data['bytes']: + if not icon_was_cached or not os.path.exists(pkg.model.get_disk_icon_path()): + self.window.manager.cache_to_disk(pkg=pkg.model, icon_bytes=icon_data['bytes'], only_icon=True) def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = True): @@ -259,13 +265,18 @@ def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = self.setColumnCount(self.COL_NUMBER if update_check_enabled else self.COL_NUMBER - 1) self.setRowCount(len(pkgs)) + file_downloader_defined = False + for idx, pkg in enumerate(pkgs): pkg.table_index = idx - if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url: - icon_request = QNetworkRequest(QUrl(pkg.model.icon_url)) - icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) - self.network_man.get(icon_request) + if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url \ + and RE_URL.match(pkg.model.icon_url): + if not file_downloader_defined: + self._setup_file_downloader() + file_downloader_defined = True + + self.file_downloader.get(pkg.model.icon_url, idx) self._update_row(pkg, screen_width, update_check_enabled) @@ -391,6 +402,16 @@ def _set_col_version(self, col: int, pkg: PackageView, screen_width: int): item.setToolTip(tooltip) self.setCellWidget(pkg.table_index, col, item) + def _read_default_icon(self, pkgv: PackageView): + icon_path = pkgv.model.get_default_icon_path() + icon = self.cache_default_icon.get(icon_path) + + if not icon: + icon = QIcon(icon_path) + self.cache_default_icon[icon_path] = icon + + return icon + def _set_col_icon(self, col: int, pkg: PackageView): icon_path = pkg.model.get_disk_icon_path() if pkg.model.installed and pkg.model.supports_disk_cache() and icon_path: @@ -403,24 +424,24 @@ def _set_col_icon(self, col: int, pkg: PackageView): icon = QIcon(pixmap) self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': icon_bytes}) else: - icon = QIcon(pkg.model.get_default_icon_path()) + icon = self._read_default_icon(pkg) else: try: icon = QIcon.fromTheme(icon_path) if icon.isNull(): - icon = QIcon(pkg.model.get_default_icon_path()) + icon = self._read_default_icon(pkg) elif pkg.model.icon_url: self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': None}) except Exception: - icon = QIcon(pkg.model.get_default_icon_path()) + icon = self._read_default_icon(pkg) elif not pkg.model.icon_url: - icon = QIcon(pkg.model.get_default_icon_path()) + icon = self._read_default_icon(pkg) else: icon_data = self.icon_cache.get(pkg.model.icon_url) - icon = icon_data['icon'] if icon_data else QIcon(pkg.model.get_default_icon_path()) + icon = icon_data['icon'] if icon_data else self._read_default_icon(pkg) col_icon = QLabel() col_icon.setProperty('icon', 'true') @@ -593,3 +614,18 @@ def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToConten def get_width(self): return reduce(operator.add, [self.columnWidth(i) for i in range(self.columnCount())]) + + def _setup_file_downloader(self, max_workers: int = 50, max_downloads: int = -1) -> None: + self.file_downloader = URLFileDownloader(logger=self.logger, + max_workers=max_workers, + max_downloads=max_downloads, + parent=self) + self.file_downloader.signal_downloaded.connect(self._update_pkg_icon) + self.file_downloader.start() + + def stop_file_downloader(self, wait: bool = False) -> None: + if self.file_downloader: + self.file_downloader.stop() + + if wait: + self.file_downloader.wait() diff --git a/bauh/view/qt/commons.py b/bauh/view/qt/commons.py index e5384a13c..919239b99 100644 --- a/bauh/view/qt/commons.py +++ b/bauh/view/qt/commons.py @@ -1,10 +1,31 @@ -from typing import Iterable, List +from typing import List, Dict, Any, NamedTuple, Optional, Union, Collection, Iterable from bauh.api.abstract.model import SoftwarePackage from bauh.view.qt.view_model import PackageView -def new_pkgs_info() -> dict: +class PackageFilters(NamedTuple): + """ + It represents the manage window selected filters + """ + + display_limit: int + category: str + name: Optional[str] + only_apps: bool + only_installed: bool + only_updates: bool + only_verified: bool + search: Optional[str] # initial search term + type: str + + @property + def anything(self) -> bool: + return not self.only_installed and not self.only_updates and not self.only_apps \ + and not self.only_verified and not self.name and self.type == "any" and self.category == "any" + + +def new_pkgs_info() -> Dict[str, Any]: return {'apps_count': 0, # number of application packages 'napps_count': 0, # number of not application packages (libraries, runtimes or something else) 'available_types': {}, # available package types in 'new_pkgs' @@ -15,10 +36,11 @@ def new_pkgs_info() -> dict: 'not_installed': 0, 'installed': 0, 'categories': set(), + 'verified': 0, 'pkgs': []} # total packages -def update_info(pkgv: PackageView, pkgs_info: dict): +def update_info(pkgv: PackageView, pkgs_info: Dict[str, Any]): pkgs_info['available_types'][pkgv.model.get_type()] = {'icon': pkgv.model.get_type_icon_path(), 'label': pkgv.get_type_label()} if pkgv.model.is_application(): @@ -26,6 +48,9 @@ def update_info(pkgv: PackageView, pkgs_info: dict): else: pkgs_info['napps_count'] += 1 + if pkgv.model.is_trustable(): + pkgs_info['verified'] += 1 + if pkgv.model.update and not pkgv.model.is_update_ignored(): if pkgv.model.is_application(): pkgs_info['app_updates'] += 1 @@ -49,8 +74,8 @@ def update_info(pkgv: PackageView, pkgs_info: dict): pkgs_info['not_installed'] += 1 -def apply_filters(pkg: PackageView, filters: dict, info: dict, limit: bool = True): - if not limit or not filters['display_limit'] or len(info['pkgs_displayed']) < filters['display_limit']: +def apply_filters(pkg: PackageView, filters: PackageFilters, info: dict, limit: bool = True): + if not limit or not filters.display_limit or len(info['pkgs_displayed']) < filters.display_limit: if not is_package_hidden(pkg, filters): info['pkgs_displayed'].append(pkg) @@ -65,41 +90,51 @@ def sum_updates_displayed(info: dict) -> int: return updates -def is_package_hidden(pkg: PackageView, filters: dict) -> bool: - hidden = filters['only_installed'] and not pkg.model.installed +def is_package_hidden(pkg: PackageView, filters: PackageFilters) -> bool: + hidden = filters.only_installed and not pkg.model.installed - if not hidden and filters['only_apps']: + if not hidden and filters.only_apps: hidden = pkg.model.installed and not pkg.model.is_application() - if not hidden and filters['updates']: + if not hidden and filters.only_updates: hidden = not pkg.model.update or pkg.model.is_update_ignored() - if not hidden and filters['type'] is not None and filters['type'] != 'any': - hidden = pkg.model.get_type() != filters['type'] + if not hidden and filters.only_verified: + hidden = not pkg.model.is_trustable() + + if not hidden and filters.type is not None and filters.type != "any": + hidden = pkg.model.get_type() != filters.type - if not hidden and filters['category'] is not None and filters['category'] != 'any': - hidden = not pkg.model.categories or not [c for c in pkg.model.categories if c.lower() == filters['category']] + if not hidden and filters.category is not None and filters.category != "any": + hidden = not pkg.model.categories or not next((c for c in pkg.model.categories if c.lower() == filters.category), None) - if not hidden and filters['name']: - hidden = not filters['name'] in pkg.model.name.lower() + if not hidden and filters.name: + hidden = not filters.name.startswith(pkg.model.name.lower()) return hidden -def sort_packages(pkgs: Iterable[SoftwarePackage], word: str, limit: int = 0) -> List[SoftwarePackage]: - exact, starts_with, contains, others = [], [], [], [] +def _by_name(pkg: Union[SoftwarePackage, PackageView]): + return pkg.name.lower() - for p in pkgs: - lower_name = p.name.lower() - if word == lower_name: - exact.append(p) - elif lower_name.startswith(word): - starts_with.append(p) - elif word in lower_name: - contains.append(p) - else: - others.append(p) +def sort_packages(pkgs: Iterable[Union[SoftwarePackage, PackageView]], word: Optional[str], + limit: int = 0) -> List[SoftwarePackage]: + exact, starts_with, contains, others = [], [], [], [] + + if not word: + others.extend(pkgs) + else: + for p in pkgs: + lower_name = p.name.lower() + if word == lower_name: + exact.append(p) + elif lower_name.startswith(word): + starts_with.append(p) + elif word in lower_name: + contains.append(p) + else: + others.append(p) res = [] for app_list in (exact, starts_with, contains, others): @@ -110,7 +145,7 @@ def sort_packages(pkgs: Iterable[SoftwarePackage], word: str, limit: int = 0) -> break to_add = app_list[0:last] - to_add.sort(key=lambda a: a.name.lower()) + to_add.sort(key=_by_name) res.extend(to_add) return res diff --git a/bauh/view/qt/dialog.py b/bauh/view/qt/dialog.py index 13ffc0f56..94de3e112 100644 --- a/bauh/view/qt/dialog.py +++ b/bauh/view/qt/dialog.py @@ -35,7 +35,8 @@ def __init__(self, title: str, body: Optional[str], i18n: I18n, icon: QIcon = QI widgets: Optional[List[QWidget]] = None, confirmation_button: bool = True, deny_button: bool = True, window_cancel: bool = False, confirmation_label: Optional[str] = None, deny_label: Optional[str] = None, confirmation_icon: bool = True, min_width: Optional[int] = None, - min_height: Optional[int] = None, max_width: Optional[int] = None): + min_height: Optional[int] = None, max_width: Optional[int] = None, + confirmation_icon_type: MessageType = MessageType.INFO): super(ConfirmationDialog, self).__init__() if not window_cancel: @@ -79,6 +80,7 @@ def __init__(self, title: str, body: Optional[str], i18n: I18n, icon: QIcon = QI if confirmation_icon: lb_icon = QLabel() lb_icon.setObjectName("confirm_dialog_icon") + lb_icon.setProperty("type", confirmation_icon_type.name.lower()) container_body.layout().addWidget(lb_icon) if body: diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index f12f2d290..01abcacc0 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -1,4 +1,8 @@ +import shutil +import subprocess from collections.abc import Iterable +from subprocess import Popen +from typing import Optional from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QCursor @@ -7,6 +11,7 @@ QHBoxLayout from bauh.api.abstract.cache import MemoryCache +from bauh.commons.regex import RE_URL from bauh.view.qt.components import new_spacer from bauh.view.qt.qt_utils import get_current_screen_geometry from bauh.view.util.translation import I18n @@ -16,10 +21,11 @@ class InfoDialog(QDialog): - def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n): + def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n, can_open_url: bool): super(InfoDialog, self).__init__() self.setWindowTitle(str(pkg_info['__app__'])) self.i18n = i18n + self._can_open_url = can_open_url layout = QVBoxLayout() self.setLayout(layout) @@ -64,7 +70,7 @@ def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n): val = str(pkg_info[attr]).strip() show_val = val - i18n_val = i18n.get('{}.{}'.format(i18n_key, val.lower())) + i18n_val = i18n.get(f"{i18n_key}.{val.lower()}") if i18n_val: val = i18n_val @@ -82,7 +88,8 @@ def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n): self.gbox_info.layout().addWidget(label, idx, 0) self.gbox_info.layout().addWidget(text, idx, 1) - self._gen_show_button(idx, show_val) + + self._gen_show_button(idx=idx, val=show_val) layout.addWidget(scroll) @@ -113,18 +120,35 @@ def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n): self.setMaximumHeight(int(screen_height * 0.8)) self.adjustSize() - def _gen_show_button(self, idx: int, val): + @staticmethod + def open_url(url: str): + Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL) + + def _show_full_field_val(self, val: str): + self.gbox_info.hide() + self.bt_back.setVisible(True) + self.text_field.show() + self.text_field.setPlainText(val) + + def _gen_show_button(self, idx: int, val: str): + + is_url = self._can_open_url and bool(RE_URL.match(val)) if val else False + + if is_url: + bt_label = self.i18n["manage_window.info.open_url"] + + def _show_field(): + self.open_url(val) + else: + bt_label = self.i18n["show"].capitalize() - def show_full_field(): - self.gbox_info.hide() - self.bt_back.setVisible(True) - self.text_field.show() - self.text_field.setPlainText(val) + def _show_field(): + self._show_full_field_val(val) - bt_full_field = QPushButton(self.i18n['show'].capitalize()) - bt_full_field.setObjectName('show') + bt_full_field = QPushButton(bt_label) + bt_full_field.setObjectName("show") bt_full_field.setCursor(QCursor(Qt.PointingHandCursor)) - bt_full_field.clicked.connect(show_full_field) + bt_full_field.clicked.connect(_show_field) self.gbox_info.layout().addWidget(bt_full_field, idx, 2) def back_to_info(self): diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py index 7b5384552..fc80fd839 100644 --- a/bauh/view/qt/prepare.py +++ b/bauh/view/qt/prepare.py @@ -5,7 +5,7 @@ from typing import Tuple, Optional from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication, QMutex -from PyQt5.QtGui import QIcon, QCursor, QCloseEvent +from PyQt5.QtGui import QIcon, QCursor, QCloseEvent, QShowEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, \ QProgressBar, QPlainTextEdit, QToolButton, QHBoxLayout @@ -133,7 +133,7 @@ def run(self): ti = datetime.datetime.now() while True: - if datetime.datetime.now() >= ti + datetime.timedelta(minutes=1.5): + if datetime.datetime.now() >= ti + datetime.timedelta(seconds=10): self.signal_timeout.emit() break @@ -253,6 +253,7 @@ def __init__(self, context: ApplicationContext, manager: SoftwareManager, self.bt_bar.add_widget(self.bt_skip) self.layout().addWidget(self.bt_bar) + centralize(self) def hide_output(self): self.current_output_task = None @@ -286,14 +287,14 @@ def _resize_columns(self): self.resize(int(self.get_table_width() * 1.05), self.sizeHint().height()) - def show(self): - super(PreparePanel, self).show() + def showEvent(self, event: Optional[QShowEvent]) -> None: + super().showEvent(event) self.prepare_thread.start() screen_size = get_current_screen_geometry() - self.setMinimumWidth(int(screen_size.width() * 0.5)) + self.setMinimumWidth(int(screen_size.width() * 0.25)) self.setMinimumHeight(int(screen_size.height() * 0.35)) self.setMaximumHeight(int(screen_size.height() * 0.95)) - centralize(self, align_top_left=False) + centralize(self) def start(self, tasks: int): self.started_at = time.time() diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py index 5d52aaf77..08a30fe0c 100644 --- a/bauh/view/qt/screenshots.py +++ b/bauh/view/qt/screenshots.py @@ -1,6 +1,8 @@ import logging +import traceback +from io import BytesIO from threading import Thread -from typing import List +from typing import List, Dict from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QCursor @@ -95,6 +97,7 @@ def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: Memory self.bt_next.setCursor(QCursor(Qt.PointingHandCursor)) self.bt_next.clicked.connect(self.next) self.container_buttons.layout().addWidget(self.bt_next) + self.download_progress: Dict[int, float] = dict() self.layout().addWidget(self.container_buttons) @@ -107,13 +110,16 @@ def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: Memory t.start() self.resize(self.max_img_width + 5, self.max_img_height + 5) - self._load_img() + self._load_img(self.img_idx) qt_utils.centralize(self) def _update_progress(self, val: int): self.progress_bar.setValue(val) - def _load_img(self): + def _load_img(self, img_idx: int): + if img_idx != self.img_idx: + return + if len(self.loaded_imgs) > self.img_idx: img = self.loaded_imgs[self.img_idx] @@ -131,7 +137,10 @@ def _load_img(self): else: self.img.setPixmap(QPixmap()) self.img.setCursor(QCursor(Qt.WaitCursor)) - self.img.setText('{} {}/{}...'.format(self.i18n['screenshots.image.loading'], self.img_idx + 1, len(self.screenshots))) + + progress = self.download_progress.get(self.img_idx, 0) + self.img.setText(f"{self.i18n['screenshots.image.loading']} " + f"{self.img_idx + 1}/{len(self.screenshots)} ({progress:.2f}%)") self.progress_bar.setVisible(True) self.thread_progress.start() @@ -142,36 +151,66 @@ def _load_img(self): self.bt_back.setEnabled(self.img_idx != 0) self.bt_next.setEnabled(self.img_idx != len(self.screenshots) - 1) - def _download_img(self, idx: int, url: str): - self.logger.info('Downloading image [{}] from {}'.format(idx, url)) - res = self.http_client.get(url) - - if res: - if not res.content: - self.logger.warning('Image [{}] from {} has no content'.format(idx, url)) - self.loaded_imgs.append(self.i18n['screenshots.download.no_content']) - self._load_img() - else: - self.logger.info('Image [{}] successfully downloaded'.format(idx)) - pixmap = QPixmap() - pixmap.loadFromData(res.content) + def _handle_download_exception(self, idx: int, url: str): + self.logger.error(f"Unexpected exception while downloading screenshot from '{url}'") + traceback.print_exc() + self.loaded_imgs.append(self.i18n["screenshots.download.no_response"]) + self._load_img(idx) - if pixmap.size().height() > self.max_img_height or pixmap.size().width() > self.max_img_width: - pixmap = pixmap.scaled(self.max_img_width, self.max_img_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) - - self.loaded_imgs.append(pixmap) - - if self.img_idx == idx: - self._load_img() + def _download_img(self, idx: int, url: str): + self.logger.info(f"Downloading image [{idx}] from {url}") + + try: + res = self.http_client.get(url=url, stream=True) + except Exception: + self._handle_download_exception(idx, url) + return + + if not res: + self.logger.info(f"Could not retrieve image [{idx}] from '{url}'") + self.loaded_imgs.append(self.i18n["screenshots.download.no_response"]) + self._load_img(idx) + return + + try: + content_length = int(res.headers.get("content-length", 0)) + except Exception: + content_length = 0 + self.logger.warning(f"Could not retrieve the content-length for file '{url}'") + + if content_length <= 0: + self.logger.warning(f"Image [{idx}] has no content ({url})") + self.loaded_imgs.append(self.i18n['screenshots.download.no_content']) + self._load_img(idx) else: - self.logger.info("Could not retrieve image [{}] from {}".format(idx, url)) - self.loaded_imgs.append(self.i18n['screenshots.download.no_response']) - self._load_img() + byte_stream = BytesIO() + + total_downloaded = 0 + try: + for data in res.iter_content(chunk_size=1024): + byte_stream.write(data) + total_downloaded += len(data) + self.download_progress[idx] = (total_downloaded / content_length) * 100 + self._load_img(idx) + except Exception: + self._handle_download_exception(idx, url) + return + + self.logger.info(f"Image [{idx}] successfully downloaded ({url})") + pixmap = QPixmap() + pixmap.loadFromData(byte_stream.getvalue()) + + if pixmap.size().height() > self.max_img_height or pixmap.size().width() > self.max_img_width: + pixmap = pixmap.scaled(self.max_img_width, self.max_img_height, Qt.KeepAspectRatio, + Qt.SmoothTransformation) + + self.loaded_imgs.append(pixmap) + self._load_img(idx) def back(self): self.img_idx -= 1 - self._load_img() + self._load_img(self.img_idx) def next(self): self.img_idx += 1 - self._load_img() + self._load_img(self.img_idx) diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py index ae732c7cd..2b1d9bdda 100644 --- a/bauh/view/qt/settings.py +++ b/bauh/view/qt/settings.py @@ -3,7 +3,7 @@ from typing import Optional from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal -from PyQt5.QtGui import QCursor +from PyQt5.QtGui import QCursor, QShowEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton, QHBoxLayout, QApplication from bauh import __app_name__ @@ -79,11 +79,12 @@ def __init__(self, manager: SoftwareManager, i18n: I18n, window: QWidget, parent self.thread_reload_panel = ReloadManagePanel(manager=manager) self.thread_reload_panel.signal_finished.connect(self._reload_manage_panel) - - def show(self): - super(SettingsWindow, self).show() centralize(self) + + def showEvent(self, event: Optional[QShowEvent]): + super(SettingsWindow, self).showEvent(event) self.setMinimumWidth(int(self.sizeHint().width())) + centralize(self) def closeEvent(self, event): if self.window and self.window.settings_window == self: diff --git a/bauh/view/qt/systray.py b/bauh/view/qt/systray.py index 1d39829f1..44228971e 100755 --- a/bauh/view/qt/systray.py +++ b/bauh/view/qt/systray.py @@ -198,7 +198,7 @@ def handle_click(self, reason): self.show_manage_window() def verify_updates(self, notify_user: bool = True): - Thread(target=self._verify_updates, args=(notify_user,)).start() + Thread(target=self._verify_updates, args=(notify_user,), daemon=True).start() def _verify_updates(self, notify_user: bool): self.notify_updates(self.manager.list_updates(), notify_user=notify_user) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 6f38ecaea..445844188 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -1,11 +1,15 @@ import os import re +import sys import time import traceback +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from io import StringIO +from logging import Logger from pathlib import Path -from typing import List, Type, Set, Tuple, Optional +from queue import Queue +from typing import List, Type, Set, Tuple, Optional, Pattern import requests from PyQt5.QtCore import QThread, pyqtSignal, QObject @@ -23,13 +27,15 @@ from bauh.api.paths import LOGS_DIR from bauh.commons.html import bold from bauh.commons.internet import InternetChecker +from bauh.commons.regex import RE_URL from bauh.commons.system import ProcessHandler, SimpleProcess from bauh.commons.view_utils import get_human_size_str from bauh.view.core import timeshift from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD from bauh.view.qt import commons -from bauh.view.qt.commons import sort_packages +from bauh.view.qt.commons import sort_packages, PackageFilters from bauh.view.qt.qt_utils import get_current_screen_geometry +from bauh.view.qt.view_index import query_packages from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util.translation import I18n @@ -956,37 +962,42 @@ def run(self): class ApplyFilters(AsyncAction): - signal_table = pyqtSignal(object) + signal_table = pyqtSignal(list) - def __init__(self, i18n: I18n, filters: dict = None, pkgs: List[PackageView] = None): + def __init__(self, i18n: I18n, logger: Logger, filters: Optional[PackageFilters] = None, + pkgs: Optional[List[PackageView]] = None, index: Optional[dict] = None): super(ApplyFilters, self).__init__(i18n=i18n) - self.pkgs = pkgs + self.logger = logger + self.index = index self.filters = filters + self.pkgs = pkgs self.wait_table_update = False def stop_waiting(self): self.wait_table_update = False def run(self): - if self.pkgs: - pkgs_info = commons.new_pkgs_info() - - name_filtering = bool(self.filters['name']) - - for pkgv in self.pkgs: - commons.update_info(pkgv, pkgs_info) - commons.apply_filters(pkgv, self.filters, pkgs_info, limit=not name_filtering) - - if name_filtering and pkgs_info['pkgs_displayed']: - pkgs_info['pkgs_displayed'] = sort_packages(word=self.filters['name'], - pkgs=pkgs_info['pkgs_displayed'], - limit=self.filters['display_limit']) + if self.index and self.filters and self.pkgs: + if self.filters.anything: + # it means no filter should be applied, and when can rely on the firstly displayed packages + sorted_pkgs = self.pkgs + + if self.filters.display_limit > 0: + sorted_pkgs = sorted_pkgs[0:self.filters.display_limit] + + else: + ti = time.time() + sort_term = self.filters.name or self.filters.search # improves displayed matches when no name typed + matched_pkgs = query_packages(index=self.index, filters=self.filters) + sorted_pkgs = commons.sort_packages(pkgs=matched_pkgs, word=sort_term) + tf = time.time() + self.logger.info(f"Took {tf - ti:.9f} seconds to filter and sort packages") self.wait_table_update = True - self.signal_table.emit(pkgs_info) + self.signal_table.emit(sorted_pkgs) while self.wait_table_update: - super(ApplyFilters, self).msleep(5) + super(ApplyFilters, self).msleep(1) self.notify_finished() @@ -1100,3 +1111,67 @@ def run(self): self.msleep(self.delay) self.signal_start.emit() + + +class URLFileDownloader(QThread): + + signal_downloaded = pyqtSignal(str, bytes, object) + + def __init__(self, logger: Logger, max_workers: int = 50, request_timeout: int = 30, inactivity_timeout: int = 3, + max_downloads: int = -1, parent: Optional[QWidget] = None): + super().__init__(parent) + self._logger = logger + self._queue = Queue() + self._max_workers = max_workers + self._request_timeout = request_timeout + self._inactivity_timeout = inactivity_timeout + self._max_downloads = max_downloads + self._stop = False + + def _get(self, url_: str, id_: Optional[object]): + if self._stop: + self._logger.info(f"File '{url_}' download cancelled") + return + + try: + res = requests.get(url=url_, timeout=self._request_timeout) + content = res.content if res.status_code == 200 else None + self.signal_downloaded.emit(url_, content, id_) + except Exception as e: + self._logger.error(f"[ERROR] could not download file from '{url_}': " + f"{e.__class__.__name__}({str(e.args)})\n") + + def run(self) -> None: + download_count = 0 + futures = [] + with ThreadPoolExecutor(max_workers=self._max_workers) as executor: + while not self._stop and (self._max_downloads <= 0 or download_count < self._max_downloads): + try: + url_, id_ = self._queue.get(timeout=self._inactivity_timeout) + futures.append(executor.submit(self._get, url_, id_)) + download_count += 1 + except Exception: + self._stop = True + + cancelled_count = 0 + for f in futures: + if not f.done(): + f.cancel() + cancelled_count += 1 + + if cancelled_count > 0: + self._logger.info(f"{cancelled_count} file downloads cancelled") + + self._logger.info(f"Finished to download files (count={download_count})") + + def stop(self) -> None: + self._stop = True + + def get(self, url: str, id_: Optional[object]): + final_url = url.strip() if url else None + + if final_url: + if RE_URL.match(final_url): + self._queue.put((final_url, id_)) + else: + self.signal_downloaded.emit(final_url, None, id_) diff --git a/bauh/view/qt/view_index.py b/bauh/view/qt/view_index.py new file mode 100644 index 000000000..0a64657ae --- /dev/null +++ b/bauh/view/qt/view_index.py @@ -0,0 +1,139 @@ +from collections import defaultdict +from typing import Dict, List, Generator, Tuple, Optional, Union + +from bauh.view.qt.commons import PackageFilters +from bauh.view.qt.view_model import PackageView + + +def new_character_idx() -> Dict[str, List[PackageView]]: + return defaultdict(list) + + +def new_category_idx() -> Dict[str, Dict[str, List[PackageView]]]: + return defaultdict(new_character_idx) + + +def new_type_index() -> Dict[str, Dict[str, Dict[str, List[PackageView]]]]: + return defaultdict(new_category_idx) + + +def new_verified_index() -> Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]: + return defaultdict(new_type_index) + + +def new_update_index() -> Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]: + return defaultdict(new_verified_index) + + +def new_app_index() -> Dict[int, Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]]: + return defaultdict(new_update_index) + + +def new_package_index() -> Dict[int, Dict[int, Dict[int, Dict[int, Dict[str, Dict[str, Dict[str, List[PackageView]]]]]]]]: + return defaultdict(new_app_index) + + +def add_to_index(pkgv: PackageView, index: dict) -> None: + # root keys: (1) installed | (0) not installed + root_idx = index[1 if pkgv.model.installed else 0] + + # app keys: (1) app | (0) not app + app_lvl = root_idx[1 if pkgv.model.is_application() else 0] + + # update keys: (1) update | (0) no update + if pkgv.model.installed and pkgv.model.update and not pkgv.model.is_update_ignored(): + update_lvl = app_lvl[1] + else: + update_lvl = app_lvl[0] + + # verified keys: (1) verified | (0) unverified + verified_lvl = update_lvl[1 if pkgv.model.is_trustable() else 0] + + norm_name = pkgv.name.strip().lower() + starts_with_chars = tuple(norm_name[0:i] for i in range(1, len(norm_name) + 1)) + + for cat in ("any", *(pkgv.model.categories if pkgv.model.categories else tuple())): + category = cat.lower().strip() + + # any type > specific category > any character (None) + verified_lvl["any"][category][None].append(pkgv) + + # any type > specific category > characters (start + for chars in starts_with_chars: + verified_lvl["any"][category][chars].append(pkgv) + + type_lvl = verified_lvl[pkgv.model.get_type()] + + # specific type > specific category > any character (None) + type_lvl[category][None].append(pkgv) + + # specific type > any category > first character + for chars in starts_with_chars: + type_lvl[category][chars].append(pkgv) + + +def generate_queries(filters: PackageFilters) -> Generator[Tuple[Optional[Union[int, str]], ...], None, None]: + chars_query = None + + if filters.name: + chars_query = filters.name.lower() + + installed_queries = (1,) if filters.only_installed else (1, 0) + apps_queries = (1,) if filters.only_apps else (1, 0) + updates_queries = (1,) if filters.only_updates else (1, 0) + verified_queries = (1,) if filters.only_verified else (1, 0) + + for installed in installed_queries: + for app in apps_queries: + for update in updates_queries: + for verified in verified_queries: + yield installed, app, update, verified, filters.type, filters.category, chars_query + + +def query_packages(index: dict, filters: PackageFilters) -> Generator[PackageView, None, None]: + yield_count = 0 + yield_limit = filters.display_limit if filters.display_limit and filters.display_limit > 0 else -1 + + queries = tuple(generate_queries(filters)) + + yielded_pkgs = defaultdict(set) + + for query in queries: + packages = index[query[0]][query[1]][query[2]][query[3]][query[4]][query[5]][query[6]] + + for pkgv in packages: + yield pkgv + yield_count += 1 + yielded_pkgs[pkgv.model.get_type()].add(pkgv.model.id) + + # checking if the package display limit has been reached + if 0 < yield_limit <= yield_count: + break + + # if there is a limit and the number of yielded packages is not reached, performs also a "contains" query + # checking if the queries target "any character" (none), if so, there is no need to perform the "contains" query + any_char_query = next((True for q in queries if q[-1] is None), False) + + if not any_char_query and 0 < yield_limit > yield_count: + for query in queries: + # checking if the package display limit has been reached + if 0 < yield_limit <= yield_count: + break + + packages = index[query[0]][query[1]][query[2]][query[3]][query[4]][query[5]][None] + + for pkgv in packages: + # checking if the package has already been yielded + yield_type_idx = yielded_pkgs.get(pkgv.model.get_type()) + if yield_type_idx and pkgv.model.id in yield_type_idx: + continue + + # checking if the package name contains the chars query + if query[5] in pkgv.model.name: + yield pkgv + yield_count += 1 + yielded_pkgs[pkgv.model.get_type()].add(pkgv.model.id) + + # checking if the package display limit has been reached + if 0 < yield_limit <= yield_count: + break diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 7a061f9c5..7fb06c86c 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -1,12 +1,13 @@ import logging import operator import os.path +import shutil import time from pathlib import Path -from typing import List, Type, Set, Tuple, Optional +from typing import List, Type, Set, Tuple, Optional, Dict, Any from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QRect -from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor, QMoveEvent +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor, QCloseEvent, QShowEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \ QLabel, QPlainTextEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \ QMenu, QHBoxLayout @@ -27,7 +28,7 @@ from bauh.view.qt import dialog, commons, qt_utils from bauh.view.qt.about import AboutDialog from bauh.view.qt.apps_table import PackagesTable, UpgradeToggleButton -from bauh.view.qt.commons import sum_updates_displayed +from bauh.view.qt.commons import sum_updates_displayed, PackageFilters from bauh.view.qt.components import new_spacer, IconButton, QtComponentsManager, to_widget, QSearchBar, \ QCustomMenuAction, QCustomToolbar from bauh.view.qt.dialog import ConfirmationDialog @@ -43,6 +44,7 @@ AsyncAction, LaunchPackage, ApplyFilters, CustomSoftwareAction, ShowScreenshots, CustomAction, \ NotifyInstalledLoaded, \ IgnorePackageUpdates, SaveTheme, StartAsyncAction +from bauh.view.qt.view_index import add_to_index, new_package_index from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util import util, resource from bauh.view.util.translation import I18n @@ -74,13 +76,14 @@ CHECK_UPDATES = 7 CHECK_APPS = 8 COMBO_TYPES = 9 -COMBO_CATEGORIES = 10 -INP_NAME = 11 -CHECK_DETAILS = 12 -BT_SETTINGS = 13 -BT_CUSTOM_ACTIONS = 14 -BT_ABOUT = 15 -BT_THEMES = 16 +CHECK_VERIFIED = 10 +COMBO_CATEGORIES = 11 +INP_NAME = 12 +CHECK_DETAILS = 13 +BT_SETTINGS = 14 +BT_CUSTOM_ACTIONS = 15 +BT_ABOUT = 16 +BT_THEMES = 17 # component groups ids GROUP_FILTERS = 1 @@ -110,6 +113,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.pkgs = [] # packages current loaded in the table self.pkgs_available = [] # all packages loaded in memory self.pkgs_installed = [] # cached installed packages + self.pkg_idx: Optional[Dict[str, Any]] = None # all packages available indexed by the available filters self.display_limit = config['ui']['table']['max_displayed'] self.icon_cache = icon_cache self.config = config @@ -175,6 +179,16 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.toolbar_filters.layout().addWidget(self.check_apps) self.comp_manager.register_component(CHECK_APPS, self.check_apps) + self.check_verified = QCheckBox() + self.check_verified.setObjectName('check_verified') + self.check_verified.setCursor(QCursor(Qt.PointingHandCursor)) + self.check_verified.setText(self.i18n['manage_window.checkbox.only_verified']) + self.check_verified.setChecked(False) + self.check_verified.stateChanged.connect(self._handle_filter_only_verified) + self.check_verified.sizePolicy().setRetainSizeWhenHidden(True) + self.toolbar_filters.layout().addWidget(self.check_verified) + self.comp_manager.register_component(CHECK_VERIFIED, self.check_verified) + self.any_type_filter = 'any' self.cache_type_filter_icons = {} self.combo_filter_type = QComboBox() @@ -277,8 +291,10 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.table_container.setLayout(QVBoxLayout()) self.table_container.layout().setContentsMargins(0, 0, 0, 0) - self.table_apps = PackagesTable(self, self.icon_cache, - download_icons=bool(self.config['download']['icons'])) + self.table_apps = PackagesTable(parent=self, + icon_cache=self.icon_cache, + download_icons=bool(self.config['download']['icons']), + logger=logger) self.table_apps.change_headers_policy() self.table_container.layout().addWidget(self.table_apps) @@ -344,7 +360,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_execute_custom_action) self.thread_screenshots = self._bind_async_action(ShowScreenshots(i18n, self.manager), finished_call=self._finish_show_screenshots) - self.thread_apply_filters = ApplyFilters(i18n) + self.thread_apply_filters = ApplyFilters(i18n=i18n, logger=logger) self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters) self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades) self.signal_table_update.connect(self.thread_apply_filters.stop_waiting) @@ -432,6 +448,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.layout.addWidget(self.container_progress) self.filter_only_apps = True + self.filter_only_verified = False self.type_filter = self.any_type_filter self.category_filter = self.any_category_filter self.filter_updates = False @@ -455,24 +472,29 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed) self._register_groups() self._screen_geometry: Optional[QRect] = None + self.searched_term: Optional[str] = None # last searched term + self._can_open_urls: Optional[bool] = None # whether URLs can be opened in the browser + + qt_utils.centralize(self) def _register_groups(self): - common_filters = (CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME) + common_filters = (CHECK_APPS, CHECK_VERIFIED, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME) self.comp_manager.register_group(GROUP_FILTERS, False, CHECK_INSTALLED, *common_filters) self.comp_manager.register_group(GROUP_VIEW_SEARCH, False, - COMBO_CATEGORIES, COMBO_TYPES, INP_NAME, # filters - BT_INSTALLED, BT_SUGGESTIONS, CHECK_INSTALLED) # buttons + CHECK_INSTALLED, CHECK_VERIFIED, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME, + BT_INSTALLED, BT_SUGGESTIONS) self.comp_manager.register_group(GROUP_VIEW_INSTALLED, False, BT_REFRESH, BT_UPGRADE, # buttons *common_filters) self.comp_manager.register_group(GROUP_UPPER_BAR, False, - CHECK_APPS, CHECK_UPDATES, CHECK_INSTALLED, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME, - BT_INSTALLED, BT_SUGGESTIONS, BT_REFRESH, BT_UPGRADE) + CHECK_APPS, CHECK_VERIFIED, CHECK_UPDATES, CHECK_INSTALLED, COMBO_CATEGORIES, + COMBO_TYPES, INP_NAME, BT_INSTALLED, BT_SUGGESTIONS, BT_REFRESH, BT_UPGRADE) - self.comp_manager.register_group(GROUP_LOWER_BTS, False, BT_SUGGESTIONS, BT_THEMES, BT_CUSTOM_ACTIONS, BT_SETTINGS, BT_ABOUT) + self.comp_manager.register_group(GROUP_LOWER_BTS, False, BT_SUGGESTIONS, BT_THEMES, BT_CUSTOM_ACTIONS, + BT_SETTINGS, BT_ABOUT) def update_custom_actions(self): self.custom_actions = [a for a in self.manager.gen_custom_actions()] @@ -498,13 +520,15 @@ def _set_table_enabled(self, enabled: bool): def begin_apply_filters(self): self.stop_notifying_package_states() + self._begin_action(action_label=self.i18n['manage_window.status.filtering'], action_id=ACTION_APPLY_FILTERS) self.comp_manager.disable_visible_from_groups(GROUP_UPPER_BAR, GROUP_LOWER_BTS) self.comp_manager.set_component_read_only(INP_NAME, True) - self.thread_apply_filters.filters = self._gen_filters() self.thread_apply_filters.pkgs = self.pkgs_available + self.thread_apply_filters.index = self.pkg_idx + self.thread_apply_filters.filters = self._gen_filters() self.thread_apply_filters.start() self.setFocus(Qt.NoFocusReason) @@ -518,8 +542,13 @@ def stop_notifying_package_states(self): self.signal_stop_notifying.emit() self.thread_notify_pkgs_ready.wait(1000) - def _update_table_and_upgrades(self, pkgs_info: dict): - self._update_table(pkgs_info=pkgs_info, signal=True) + def _update_table_and_upgrades(self, packages_displayed: List[PackageView]): + # generating a mocked info to keep compatibility with '_update_table' inputs. + # 'not_installed' is only used for a quick check and does not require an exact number (only 0 or 1) + info = {"pkgs_displayed": packages_displayed, + "not_installed": 1 if len(self.pkgs_installed) != len(self.pkgs_available) else 0} + + self._update_table(pkgs_info=info, signal=True) if self.pkgs: self._update_state_when_pkgs_ready() @@ -583,21 +612,21 @@ def _show_warnings(self, warnings: List[str]): if warnings: dialog.show_message(title=self.i18n['warning'].capitalize(), body='

{}

'.format('

'.join(warnings)), type_=MessageType.WARNING) - def show(self): - super(ManageWindow, self).show() - + def showEvent(self, event: Optional[QShowEvent]) -> None: + super().showEvent(event) if not self.thread_warnings.isFinished(): self.thread_warnings.start() - qt_utils.centralize(self) self._screen_geometry = get_current_screen_geometry() self._update_size_limits() + qt_utils.centralize(self) def verify_warnings(self): self.thread_warnings.start() def _begin_loading_installed(self): if self.installed_loaded: + self.table_apps.stop_file_downloader() self.search_bar.clear() self.input_name.set_text('') self._begin_action(self.i18n['manage_window.status.installed']) @@ -642,6 +671,10 @@ def _handle_filter_only_apps(self, status: int): self.filter_only_apps = status == 2 self.begin_apply_filters() + def _handle_filter_only_verified(self, status: int): + self.filter_only_verified = status == 2 + self.begin_apply_filters() + def _handle_filter_only_installed(self, status: int): self.filter_installed = status == 2 self.begin_apply_filters() @@ -725,6 +758,7 @@ def _handle_console_option(self, enable: bool): self.textarea_details.hide() def begin_refresh_packages(self, pkg_types: Optional[Set[Type[SoftwarePackage]]] = None): + self.table_apps.stop_file_downloader() self.search_bar.clear() self._begin_action(self.i18n['manage_window.status.refreshing']) @@ -752,6 +786,10 @@ def _finish_refresh_packages(self, res: dict, as_installed: bool = True): self._update_bts_installed_and_suggestions() self._reorganize() + if self.first_refresh: + self.first_refresh = False + qt_utils.centralize(self) + self.load_suggestions = False self.types_changed = False @@ -761,6 +799,7 @@ def load_without_packages(self): self._finish_refresh_packages({'installed': None, 'types': None}, as_installed=False) def begin_load_suggestions(self, filter_installed: bool): + self.table_apps.stop_file_downloader() self.search_bar.clear() self._begin_action(self.i18n['manage_window.status.suggestions']) self._handle_console_option(False) @@ -838,6 +877,7 @@ def _finish_uninstall(self, res: dict): self.update_custom_actions() self._show_console_checkbox_if_output() self._update_installed_filter() + self._update_index() self.begin_apply_filters() self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized) self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized) @@ -854,6 +894,13 @@ def _update_table_indexes(self): for new_idx, pkgv in enumerate(self.pkgs): # updating the package indexes pkgv.table_index = new_idx + def _update_index(self): + if self.pkgs_available: + idx = new_package_index() + for pkgv in self.pkgs_available: + add_to_index(pkgv, idx) + self.pkg_idx = idx + def begin_launch_package(self, pkg: PackageView): self._begin_action(action_label=self.i18n['manage_window.status.running_app'].format(pkg.model.name), action_id=ACTION_LAUNCH) @@ -884,9 +931,9 @@ def _reorganize(self): self._resize(accept_lower_width=len(self.pkgs) > 0) def _update_table(self, pkgs_info: dict, signal: bool = False): - self.pkgs = pkgs_info['pkgs_displayed'] + self.pkgs = pkgs_info["pkgs_displayed"] - if pkgs_info['not_installed'] == 0: + if pkgs_info["not_installed"] == 0: update_check = sum_updates_displayed(pkgs_info) > 0 else: update_check = False @@ -900,9 +947,9 @@ def _update_table(self, pkgs_info: dict, signal: bool = False): self._resize(accept_lower_width=len(self.pkgs) > 0) if len(self.pkgs) == 0 and len(self.pkgs_available) == 0: - self.label_displayed.setText('') + self.label_displayed.setText("") else: - self.label_displayed.setText('{} / {}'.format(len(self.pkgs), len(self.pkgs_available))) + self.label_displayed.setText(f"{len(self.pkgs)} / {len(self.pkgs_available)}") else: self.label_displayed.hide() @@ -952,20 +999,21 @@ def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, setattr(self, attr, checked) checkbox.blockSignals(False) - def _gen_filters(self, ignore_updates: bool = False) -> dict: - return { - 'only_apps': False if self.search_performed else self.filter_only_apps, - 'type': self.type_filter, - 'category': self.category_filter, - 'updates': False if ignore_updates else self.filter_updates, - 'name': self.input_name.text().lower() if self.input_name.text() else None, - 'display_limit': None if self.filter_updates else self.display_limit, - 'only_installed': self.filter_installed - } + def _gen_filters(self, ignore_updates: bool = False) -> PackageFilters: + return PackageFilters(category=self.category_filter, + display_limit=0 if self.filter_updates else self.display_limit, + name=self.input_name.text().strip(), + only_apps=False if self.search_performed else self.filter_only_apps, + only_verified=self.filter_only_verified, + only_updates=False if ignore_updates else self.filter_updates, + only_installed=self.filter_installed, + search=self.searched_term, + type=self.type_filter) def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: bool, types: Optional[Set[type]] = None, ignore_updates: bool = False, keep_filters: bool = False) -> bool: self.input_name.set_text('') pkgs_info = commons.new_pkgs_info() + pkg_idx = new_package_index() filters = self._gen_filters(ignore_updates=ignore_updates) if new_pkgs is not None: @@ -976,19 +1024,22 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b self.pkgs_installed = [] for pkg in new_pkgs: - app_model = PackageView(model=pkg, i18n=self.i18n) - commons.update_info(app_model, pkgs_info) - commons.apply_filters(app_model, filters, pkgs_info) + pkgv = PackageView(model=pkg, i18n=self.i18n) + commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) + commons.apply_filters(pkgv, filters, pkgs_info) if old_installed and types: for pkgv in old_installed: if pkgv.model.__class__ not in types: commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) commons.apply_filters(pkgv, filters, pkgs_info) else: # use installed for pkgv in self.pkgs_installed: commons.update_info(pkgv, pkgs_info) + add_to_index(pkgv, pkg_idx) commons.apply_filters(pkgv, filters, pkgs_info) if pkgs_info['apps_count'] == 0 and not self.suggestions_requested: @@ -1009,6 +1060,7 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b self.check_apps.setCheckable(True) self._change_checkbox(self.check_apps, True, 'filter_only_apps', trigger=False) + self._update_verified_filter(verified_available=pkgs_info['verified'] > 0, keep_state=keep_filters) self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) @@ -1016,6 +1068,7 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed'])) self.pkgs_available = pkgs_info['pkgs'] + self.pkg_idx = pkg_idx if as_installed: self.pkgs_installed = pkgs_info['pkgs'] @@ -1034,10 +1087,6 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b self._resize(accept_lower_width=bool(self.pkgs_installed)) - if self.first_refresh: - qt_utils.centralize(self) - self.first_refresh = False - if not self.installed_loaded and as_installed: self.installed_loaded = True @@ -1064,6 +1113,19 @@ def _update_installed_filter(self, keep_state: bool = True, hide: bool = False, else: self.comp_manager.set_component_visible(CHECK_INSTALLED, has_installed) + def _update_verified_filter(self, keep_state: bool = True, verified_available: Optional[bool] = None): + if verified_available is not None: + has_verified = verified_available + else: + has_verified = False + if self.pkgs_available: + has_verified = next((True for p in self.pkgs_available if p.model.is_trustable()), False) + + if not keep_state or not has_verified: + self._change_checkbox(self.check_verified, False, 'filter_only_verified', trigger=False) + + self.comp_manager.set_component_visible(CHECK_VERIFIED, has_verified) + def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): pkgs_info['pkgs_displayed'] = [] filters = self._gen_filters(ignore_updates=ignore_updates) @@ -1265,8 +1327,9 @@ def begin_downgrade(self, pkg: PackageView): if not proceed: return - self._begin_action(action_label='{} {}'.format(self.i18n['manage_window.status.downgrading'], pkg.model.name), - action_id=ACTION_DOWNGRADE) + self.table_apps.stop_file_downloader() + label = f"{self.i18n['manage_window.status.downgrading']} {pkg.model.name}" + self._begin_action(action_label=label, action_id=ACTION_DOWNGRADE) self.comp_manager.set_components_visible(False) self._handle_console_option(True) @@ -1307,7 +1370,8 @@ def _finish_show_info(self, pkg_info: dict): if pkg_info: if len(pkg_info) > 1: - dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n) + dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n, + can_open_url=self.can_open_urls) dialog_info.exec_() else: dialog.show_message(title=self.i18n['warning'].capitalize(), @@ -1360,17 +1424,17 @@ def _finish_show_history(self, res: dict): dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) dialog_history.exec_() - def _begin_search(self, word, action_id: int = None): - self.filter_updates = False - self.filter_installed = False - self._begin_action('{} {}'.format(self.i18n['manage_window.status.searching'], word if word else ''), action_id=action_id) - def search(self): word = self.search_bar.text().strip() if word: + self.table_apps.stop_file_downloader() self._handle_console(False) - self._begin_search(word, action_id=ACTION_SEARCH) + self.filter_updates = False + self.filter_installed = False + label = f"{self.i18n['manage_window.status.searching']} {word if word else ''}" + self._begin_action(action_label=label, action_id=ACTION_SEARCH) self.comp_manager.set_components_visible(False) + self.searched_term = word self.thread_search.word = word self.thread_search.start() @@ -1510,6 +1574,7 @@ def _finish_install(self, res: dict): self.update_custom_actions() self._update_installed_filter(installed_available=True, keep_state=True) + self._update_index() self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized) self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized) self._resize(accept_lower_width=False) @@ -1545,6 +1610,9 @@ def begin_execute_custom_action(self, pkg: Optional[PackageView], action: Custom else: action_label += f' {pkg.model.name}' + if action.refresh: + self.table_apps.stop_file_downloader() + self._begin_action(action_label=action_label, action_id=ACTION_CUSTOM_ACTION) self.comp_manager.set_components_visible(False) self._handle_console_option(True) @@ -1590,7 +1658,6 @@ def show_settings(self): self.settings_window.setMinimumWidth(int(screen_width / 4)) self.settings_window.resize(self.size()) self.settings_window.adjustSize() - qt_utils.centralize(self.settings_window) self.settings_window.show() def _map_custom_action(self, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction: @@ -1666,6 +1733,7 @@ def finish_ignore_updates(self, res: dict): break self._add_pkg_categories(res['pkg']) + self._update_index() dialog.show_message(title=self.i18n['success'].capitalize(), body=self.i18n['action.{}.success'.format(res['action'])].format(bold(res['pkg'].model.name)), @@ -1747,3 +1815,14 @@ def _reload(self): self.verify_warnings() self.types_changed = True self.begin_refresh_packages() + + def closeEvent(self, event: QCloseEvent) -> None: + # needs to be stopped to avoid a Qt exception/crash + self.table_apps.stop_file_downloader(wait=True) + + @property + def can_open_urls(self) -> bool: + if self._can_open_urls is None: + self._can_open_urls = shutil.which("xdg-open") + + return self._can_open_urls diff --git a/bauh/view/resources/__init__.py b/bauh/view/resources/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/img/__init__.py b/bauh/view/resources/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/locale/__init__.py b/bauh/view/resources/locale/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/locale/about/__init__.py b/bauh/view/resources/locale/about/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 50abfaf6e..a6c8be57d 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -66,6 +66,7 @@ action.ignore_updates.success=Updates from {} will be ignored from now on action.ignore_updates_reverse.fail=It was not possible to revert ignored updates from {} action.ignore_updates_reverse.success=Updates from {} will be displayed again from now on action.info.tooltip=Feu clic aquí per a veure informació sobre aquesta aplicació +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Action not allowed action.request_reboot.title=System restart action.reset=Restore @@ -112,50 +113,101 @@ cancel=cancel·la categories=categories category=categoria category.2dgraphics=gràfics 2d +category.3dgraphics=gràfics 3d +category.accessibility=accessibility category.acessories=acessories +category.actiongame=action game category.admin=admin category.administration=administració +category.adult=adult +category.adventuregame=adventure game category.amusement=diversió category.application=application category.arcadegame=joc «arcade» category.archiving=archiving category.art=art +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=àudio category.audiovideo=àudio/vídeo +category.audiovideoediting=àudio/vídeo editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=llibres category.browser=navegador +category.building=building +category.calendar=calendar category.calculator=calculator +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=núvol category.communication=comunicació category.compression=compression +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=core category.database=base de dades category.databases=bases de dades +category.datavisualization=data visualization +category.debugger=debugger category.design=disseny category.desktopsettings=paràmetres de l’escriptori category.devel=desenvolupament category.development=desenvolupament +category.dialup=dial-up category.devices=dispositius category.dictionary=dictionary +category.discburning=disc burning category.doc=documentation +category.documentation=documentation +category.economy=economy category.education=educació +category.electricity=electricity +category.electronics=electronics category.emulator=emulador +category.engineering=engineering category.entertainment=entreteniment +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=transferència de fitxers category.finance=finances category.fitness=condició física +category.flowchart=flow chart category.fonts=tipus de lletra category.game=joc category.games=jocs +category.geography=geography +category.geology=geology +category.geoscience=geoscience category.graphics=gràfics +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.health=salut +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=missatgeria +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=biblioteca category.library=biblioteca +category.literature=literature category.localization=localization +category.logicgame=logic game +category.maps=maps category.math=math category.media=mitjans +category.medicalsoftware=medical category.messaging=missatgeria +category.mixer=mixer category.monitor=monitoring category.movie=pel·lícula category.movies=pel·lícules @@ -165,37 +217,67 @@ category.network=xarxa category.networks=xarxes category.news=notícies category.none=None +category.numericalanalysis=numerical analysis category.office=oficina category.packagemanager=gestor de paquets +category.parallelcomputing=parallel computing category.personalisation=personalització category.photo=foto category.photography=foto -category.player=jugador +category.physics=physics +category.player=reproductor category.presentation=presentation +category.printing=printing category.productivity=productivitat +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=referència +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=ciència +category.screensaver=screensaver category.security=seguretat +category.sequencer=sequencer category.server=servidor category.settings=configuració +category.shooter=shooter game +category.simulation=simulation game category.social=social category.sound=sound +category.spirituality=spirituality +category.sports=esport category.sportsgame=joc d’esport +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=sistema +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=terminal category.text editor=editor de text category.texteditor=editor de text +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Updates ignored category.utilities=utilitats category.utility=utilitat category.vectorgraphics=gràfics vectorials category.video=vídeo +category.videoconference=video conference category.videoeditor=editor de vídeo category.viewer=visor category.weather=el temps category.web=web category.webbrowser=navegador category.webdevelopment=desenvolupament web +category.wordprocessor=word processor change=modifica clean=netejar close=tanca @@ -344,11 +426,13 @@ manage_window.bt_themes.tip=Click here to choose a theme manage_window.bt_themes.option.invalid=Invalid manage_window.checkbox.only_apps=Aplicacions manage_window.checkbox.only_installed=Instal·lats +manage_window.checkbox.only_verified=Verificats manage_window.checkbox.show_details=Mostra detalls manage_window.columns.installed=Installed manage_window.columns.latest_version=Versió més recent manage_window.columns.update=Voleu actualitzar? manage_window.info.no_info=There is no information available for {} +manage_window.info.open_url=Obrir manage_window.label.apps_displayed.tip=Applications displayed / available manage_window.label.updates=Actualitzacions manage_window.name_filter.button_tooltip=Click here to filter apps by name diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 6826140db..98aeccb1d 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -66,6 +66,7 @@ action.ignore_updates.success=Updates von {} werden an jetzt ignoriert action.ignore_updates_reverse.fail=Es war nicht möglich, ignorierte Aktualisierungen von {} rückgängig zu machen action.ignore_updates_reverse.success=Aktualisierungen von {} werden ab jetzt wieder angezeigt action.info.tooltip=Klicken Sie hier, um Informationen über diese Anwendung anzuzeigen +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Aktion nicht erlaubt action.request_reboot.title=System neustarten action.reset=Wiederherstellen @@ -111,51 +112,102 @@ bt.not_now=Nicht jetzt cancel=Abbrechen categories=Kategorien category.2dgraphics=2D-Grafik +category.3dgraphics=3D-Grafik +category.accessibility=accessibility category.acessories=Zubehör +category.actiongame=action game category.admin=Administrator category.administration=Verwaltung +category.adult=adult +category.adventuregame=adventure game category.amusement=Vergnügen category.application=Anwendung category.arcadegame=Arcade-Spiel category.archiving=Archivierung category.art=Kunst +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=Audio category.audiovideo=Audio/Video +category.audiovideoediting=audio/video editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=Bücher category.browser=Browser +category.building=building +category.calendar=calendar category.calculator=Taschenrechner +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=Cloud category.communication=Kommunikation category.compression=Komprimierung +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=Kern category.database=Datenbank category.databases=Datenbanken +category.datavisualization=data visualization +category.debugger=debugger category.design=Design category.desktopsettings=Desktop-Einstellungen category.devel=Entwicklung category.development=Entwicklung +category.dialup=dial-up category.dictionary=Wörterbuch -category.doc=Dokumentation +category.discburning=disc burning +category.doc=dokumentation +category.documentation=dokumentation +category.economy=economy category.education=Bildung +category.electricity=electricity +category.electronics=electronics category.emulator=Emulator +category.engineering=engineering category.entertainment=Unterhaltung +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=Dateiübertragung category.finance=Finanzen category.fitness=Fitness +category.flowchart=flow chart category.fonts=Schriftarten category.game=Spiel category.games=Spiele +category.geography=geography +category.geology=geology +category.geoscience=geoscience +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.graphics=Graphiken category.hardwaresettings=Hardware category.health=Gesundheit +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=Nachrichtensofortversand +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=Bibliothek category.library=Bibliothek +category.literature=literature category.localization=Lokalisation +category.logicgame=logic game +category.maps=maps category.math=Mathematik category.mail=E-Mail category.media=Medien +category.medicalsoftware=medical category.messaging=Nachrichtenübermittlung +category.mixer=mixer category.monitor=Überwachung category.movie=Film category.movies=Filme @@ -165,37 +217,67 @@ category.network=Netzwerk category.networks=Netzwerke category.news=Nachrichten category.none=Keine +category.numericalanalysis=numerical analysis category.office=Büro category.packagemanager=Paketmanager +category.parallelcomputing=parallel computing category.personalisation=Personalisierung category.photo=Foto category.photography=Foto +category.physics=physics category.player=Spieler category.presentation=Präsentation +category.printing=printing category.productivity=Produktivität +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=Referenz +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=Wissenschaft +category.screensaver=screensaver category.security=Sicherheit +category.sequencer=sequencer category.server=Server category.settings=Einstellungen +category.shooter=shooter game +category.simulation=simulation game category.social=Sozial category.sound=Sound +category.spirituality=spirituality +category.sports=sports category.sportsgame=Sportspiel +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=System +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=Terminal category.text editor=Textverarbeitung category.texteditor=Textverarbeitung +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Updates ignoriert category.utilities=Dienstprogramme category.utility=Dienstprogramm category.vectorgraphics=Vektorgrafik category.video=Video +category.videoconference=video conference category.videoeditor=Videoverarbeitung category.viewer=Betrachter category.weather=Wetter category.web=Web category.webbrowser=Browser category.webdevelopment=Web-Entwicklung +category.wordprocessor=word processor category=Kategorie change=Ändern clean=Bereinigen @@ -346,11 +428,13 @@ manage_window.bt_themes.tip=Klicken Sie hier, um ein Thema auszuwählen manage_window.bt_themes.option.invalid=Ungültig manage_window.checkbox.only_apps=Anwendungen manage_window.checkbox.only_installed=Installierte +manage_window.checkbox.only_verified=Verifiziert manage_window.checkbox.show_details=Details anzeigen manage_window.columns.installed=Installiert manage_window.columns.latest_version=Neueste Version manage_window.columns.update=Upgraden? manage_window.info.no_info=Es sind keine Informationen verfügbar für {} +manage_window.info.open_url=Öffnen manage_window.label.apps_displayed.tip=Anwendungen angezeigt / verfügbar manage_window.label.updates=Aktualisierungen manage_window.name_filter.button_tooltip=Klicken Sie hier, um Anwendungen nach Namen zu filtern diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index caff3dc1f..113f6cfb7 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -66,6 +66,7 @@ action.ignore_updates.success=Updates from {} will be ignored from now on action.ignore_updates_reverse.fail=It was not possible to revert ignored updates from {} action.ignore_updates_reverse.success=Updates from {} will be displayed again from now on action.info.tooltip=Click here to see information about this application +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Action not allowed action.request_reboot.title=System restart action.reset=Restore @@ -111,51 +112,102 @@ bt.not_now=Not now cancel=cancel categories=categories category.2dgraphics=2d graphics +category.3dgraphics=3d graphics +category.accessibility=accessibility category.acessories=acessories +category.actiongame=action game category.admin=admin category.administration=administration +category.adult=adult +category.adventuregame=adventure game category.amusement=amusement category.application=application category.arcadegame=arcade game category.archiving=archiving category.art=art +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=audio -category.audiovideo=audio / video +category.audiovideo=audio/video +category.audiovideoediting=audio/video editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=books category.browser=browser +category.building=building +category.calendar=calendar category.calculator=calculator +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=cloud category.communication=communication category.compression=compression +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=core category.database=database category.databases=databases +category.datavisualization=data visualization +category.debugger=debugger category.design=design category.desktopsettings=desktop settings category.devel=development category.development=development +category.dialup=dial-up category.dictionary=dictionary +category.discburning=disc burning category.doc=documentation +category.documentation=documentation +category.economy=economy category.education=education +category.electricity=electricity +category.electronics=electronics category.emulator=emulator +category.engineering=engineering category.entertainment=entertainment +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=file transfer category.finance=finance category.fitness=fitness +category.flowchart=flow chart category.fonts=fonts category.game=game category.games=games +category.geography=geography +category.geology=geology +category.geoscience=geoscience +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.graphics=graphics category.hardwaresettings=hardware category.health=health +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=messaging +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=library category.library=library +category.literature=literature category.localization=localization +category.logicgame=logic game +category.maps=maps category.math=math category.mail=e-mail category.media=media +category.medicalsoftware=medical category.messaging=messaging +category.mixer=mixer category.monitor=monitoring category.movie=movie category.movies=movies @@ -164,38 +216,68 @@ category.net=network category.network=network category.networks=networks category.news=news -category.none=None +category.none=none +category.numericalanalysis=numerical analysis category.office=office category.packagemanager=package manager +category.parallelcomputing=parallel computing category.personalisation=personalization category.photo=photography category.photography=photography +category.physics=physics category.player=player category.presentation=presentation +category.printing=printing category.productivity=productivity +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=reference +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=science +category.screensaver=screensaver category.security=security +category.sequencer=sequencer category.server=server category.settings=settings +category.shooter=shooter game +category.simulation=simulation game category.social=social category.sound=sound +category.spirituality=spirituality +category.sports=sports category.sportsgame=sports game +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=system +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=terminal category.text editor=text editor category.texteditor=text editor +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Updates ignored category.utilities=utilities category.utility=utility category.vectorgraphics=vector graphics category.video=video +category.videoconference=video conference category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=web category.webbrowser=browser category.webdevelopment=web development +category.wordprocessor=word processor category=category change=change clean=clean @@ -346,11 +428,13 @@ manage_window.bt_themes.tip=Click here to choose a theme manage_window.bt_themes.option.invalid=Invalid manage_window.checkbox.only_apps=Apps manage_window.checkbox.only_installed=Installed +manage_window.checkbox.only_verified=Verified manage_window.checkbox.show_details=Show details manage_window.columns.installed=Installed manage_window.columns.latest_version=Latest Version manage_window.columns.update=Upgrade ? manage_window.info.no_info=There is no information available for {} +manage_window.info.open_url=Open manage_window.label.apps_displayed.tip=Applications displayed / available manage_window.label.updates=Updates manage_window.name_filter.button_tooltip=Click here to filter apps by name diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index 9ac06a98f..31b21e6c6 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -66,6 +66,7 @@ action.ignore_updates.success=Las actualizaciones de {} serán ignoradas de ahor action.ignore_updates_reverse.fail=No fue posible revertir las actualizaciones ignoradas de {} action.ignore_updates_reverse.success=Las actualizaciones de {} se mostrarán nuevamente a partir de ahora action.info.tooltip=Pulse aquí para ver información sobre esta aplicación +action.install.unverified.warning=Este ap no ha sido verificado por los mantenedores de este sistema ni por una fuente confiable. Verifique su información antes de continuar. action.not_allowed=Acción no permitida action.request_reboot.title=Reinicio de sistema action.reset=Restaurar @@ -112,50 +113,101 @@ cancel=cancelar categories=categorías category=categoría category.2dgraphics=gráficos 2d +category.3dgraphics=gráficos 3d +category.accessibility=accesibilidad category.acessories=accesorios +category.actiongame=juego de acción category.admin=administrador category.administration=administración +category.adult=adulto +category.adventuregame=juego de aventura category.amusement=diversión category.application=aplicación category.applications=aplicaciones category.arcadegame=juego de máquina category.archiving=archivo category.art=arte +category.artificialintelligence=inteligencia artificial +category.astronomy=astronomía category.audio=audio -category.audiovideo=audio y vídeo +category.audiovideo=audio/vídeo +category.audiovideoediting=edición de audio/vídeo +category.biology=biología +category.blocksgame=juego de bloques +category.boardgame=juego de mesa category.books=libros category.browser=navegador +category.building=compilación +category.calendar=calendario category.calculator=calculadora +category.cardgame=juego de cartas +category.chart=gráfico +category.chemistry=química +category.clock=reloj category.cloud=nube category.communication=comunicación category.compression=compresión +category.computerscience=informática +category.consoleonly=solo consola +category.construction=construcción +category.contactmanagement=contactos category.core=núcleo category.database=base de datos category.databases=bases de datos +category.datavisualization=visualización de datos +category.debugger=depurador category.design=diseño category.desktopsettings=configuraciones category.devel=desarrollo category.development=desarrollo +category.dialup=marcación category.dictionary=diccionario +category.discburning=grabador de disco category.doc=documentación +category.documentation=documentación +category.economy=economía category.education=educación +category.electricity=electricidad +category.electronics=electrónica category.emulator=emulador +category.engineering=ingeniería category.entertainment=entretenimiento +category.feed=feed +category.filemanager=administrador de archivos +category.filesystem=sistema de archivos +category.filetools=herramientas de archivo category.filetransfer=transferencia de archivos category.finance=finanzas category.fitness=condición física +category.flowchart=diagrama de flujo category.fonts=tipos de letra category.game=juego category.games=juegos +category.geography=geografía +category.geology=geología +category.geoscience=geociencia category.graphics=gráficos +category.hamradio=radioaficionado +category.hardwaresettings=configuración de hardware category.health=salud +category.history=historia +category.humanities=humanidades +category.imageprocessing=procesamiento de imágenes category.instantmessaging=mensajería +category.ircclient=IRC +category.kidsgame=juego de niños +category.languages=idiomas category.libs=biblioteca category.library=biblioteca +category.literature=literatura category.localization=localización +category.logicgame=juego de lógica +category.maps=mapas category.math=matemáticas category.media=media +category.medicalsoftware=médico category.messaging=mensajería +category.mixer=mixer category.monitor=monitoreo category.movie=película category.movies=películas @@ -165,37 +217,67 @@ category.network=red category.networks=redes category.news=noticias category.none=ninguna +category.numericalanalysis=análisis numérico category.office=oficina category.packagemanager=administrador de paquetes +category.parallelcomputing=computación paralela category.personalisation=personalización category.photo=fotografía category.photography=fotografía -category.player=jugador +category.physics=física +category.player=reproductor category.presentation=presentación +category.printing=impresión category.productivity=productividad +category.profiling=desempeño +category.projectmanagement=proyectos +category.publishing=publicación +category.rastergraphics=gráficos rasterizados +category.recorder=grabador +category.remoteaccess=accesso remoto category.reference=referencia +category.revisioncontrol=control de versión +category.robotics=robótica +category.roleplaying=juego de RPG +category.scanning=escaneo category.science=ciencias +category.screensaver=protector de pantalla category.security=seguridad +category.sequencer=secuenciador category.server=servidor category.settings=configuraciones +category.shooter=juego de disparos +category.simulation=juego de simulación category.social=social category.sound=sonido +category.spirituality=espiritualidad +category.sports=deportes category.sportsgame=juego de deportes +category.spreadsheet=hoja de cálculo +category.strategygame=jogo de estrategia category.system=sistema +category.telephony=telefonía +category.telephonytools=herramientas de telefonía category.terminalemulator=terminal category.text editor=text editor category.texteditor=editor de texto +category.texttools=herramientas de texto +category.translation=traducción +category.trayicon=icono de bandeja +category.tuner=sintonizador category.updates_ignored=Actualizaciones ignoradas category.utilities=utilidades category.utility=utilidad category.vectorgraphics=gráficos vectoriales category.video=vídeo +category.videoconference=video conferencia category.videoeditor=editor de vídeos category.viewer=viewer category.weather=tiempo category.web=web category.webbrowser=navegador category.webdevelopment=desarrollo web +category.wordprocessor=editor de texto change=cambiar clean=limpiar close=cerrar @@ -345,11 +427,13 @@ manage_window.bt_themes.tip=Pulse aquí para elegir un tema manage_window.bt_themes.option.invalid=Inválido manage_window.checkbox.only_apps=Aplicaciones manage_window.checkbox.only_installed=Instalados +manage_window.checkbox.only_verified=Verificados manage_window.checkbox.show_details=Mostrar detalles manage_window.columns.installed=Instaladas manage_window.columns.latest_version=Versión más reciente manage_window.columns.update=¿Quiere actualizar? manage_window.info.no_info=No hay información disponible para {} +manage_window.info.open_url=Abrir manage_window.label.apps_displayed.tip=Applicaciones mostradas / disponibles manage_window.label.updates=Actualizaciones manage_window.name_filter.button_tooltip=Haga clic aquí para filtrar aplicaciones por nombre diff --git a/bauh/view/resources/locale/fr b/bauh/view/resources/locale/fr index 5b0a2a727..62bc472e7 100644 --- a/bauh/view/resources/locale/fr +++ b/bauh/view/resources/locale/fr @@ -66,6 +66,7 @@ action.ignore_updates.success=Les mises à jour de {} seront désormais ignorée action.ignore_updates_reverse.fail=Il était impossible d'annuler les mises à jour de {} action.ignore_updates_reverse.success=Les mises à jour de {} sont désormais de retour action.info.tooltip=Cliquez ici pour plus d'informations sur cette application +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Action non permise action.request_reboot.title=Redémarrage du système action.reset=Restaurer @@ -111,49 +112,100 @@ bt.not_now=Pas maintenant cancel=Annuler categories=categories category.2dgraphics=graphisqme 2d +category.3dgraphics=graphisqme 3d +category.accessibility=accessibility category.acessories=acessories +category.actiongame=action game category.admin=admin category.administration=administration +category.adult=adult +category.adventuregame=adventure game category.amusement=amusement category.application=application category.arcadegame=jeu d'arcade category.archiving=archiving category.art=art +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=son -category.audiovideo=son / video +category.audiovideo=son/video +category.audiovideoediting=son/video editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=livres category.browser=navigateur +category.building=building +category.calendar=calendar category.calculator=calculator +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=cloud category.communication=communication category.compression=compression +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=core category.database=base de données category.databases=bases de données +category.datavisualization=data visualization +category.debugger=debugger category.design=design category.desktopsettings=configuration category.devel=développement category.development=développement +category.dialup=dial-up category.dictionary=dictionary +category.discburning=disc burning category.doc=documentation +category.documentation=documentation +category.economy=economy category.education=éducation +category.electricity=electricity +category.electronics=electronics category.emulator=émulateur +category.engineering=engineering category.entertainment=divertissement +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=transfert de fichiers category.finance=finances category.fitness=sport +category.flowchart=flow chart category.fonts=polices category.game=jeu category.games=jeux +category.geography=geography +category.geology=geology +category.geoscience=geoscience category.graphics=graphiques +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.health=santé +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=messagerie instantanée +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=bibliothèque category.library=bibliothèque +category.literature=literature category.localization=localization +category.logicgame=logic game +category.maps=maps category.math=math category.media=media +category.medicalsoftware=medical category.messaging=messagerie +category.mixer=mixer category.monitor=monitoring category.movie=film category.movies=films @@ -163,37 +215,67 @@ category.network=réseau category.networks=réseaux category.news=presse category.none=Aucun +category.numericalanalysis=numerical analysis category.office=bureautique category.packagemanager=gestionnaire de paquets +category.parallelcomputing=parallel computing category.personalisation=personalisation category.photo=photo category.photography=photo +category.physics=physics category.player=lecteur category.presentation=presentation +category.printing=printing category.productivity=productivité +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=reference +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=science +category.screensaver=screensaver category.security=securité +category.sequencer=sequencer category.server=serveur category.settings=paramères +category.shooter=shooter game +category.simulation=simulation game category.social=social category.sound=sound +category.spirituality=spirituality +category.sports=sports category.sportsgame=jeu de sports +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=système +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=terminal category.text editor=editeur de texte category.texteditor=editeur de texte +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Mises à jour ignorées category.utilities=utilitaires category.utility=utilitaire category.vectorgraphics=graphisme vectoriel category.video=vidéo +category.videoconference=video conference category.videoeditor=éditeur vidéo category.viewer=viewer category.weather=météo category.web=web category.webbrowser=navigateur category.webdevelopment=développement web +category.wordprocessor=word processor category=categorie change=changement clean=vider @@ -342,11 +424,13 @@ manage_window.bt_themes.tip=Click here to choose a theme manage_window.bt_themes.option.invalid=Invalid manage_window.checkbox.only_apps=Apps manage_window.checkbox.only_installed=Installées +manage_window.checkbox.only_verified=Vérifiés manage_window.checkbox.show_details=Détails manage_window.columns.installed=Installé manage_window.columns.latest_version=Dernière Version manage_window.columns.update=Mettre à jour ? manage_window.info.no_info=There is no information available for {} +manage_window.info.open_url=Ouvrir manage_window.label.updates=Mises à jour manage_window.name_filter.placeholder=Filtrer par nom manage_window.name_filter.tooltip=Taper ici pour filtrer les applications par nom diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 30617852b..e57602128 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -66,6 +66,7 @@ action.ignore_updates.success=Updates from {} will be ignored from now on action.ignore_updates_reverse.fail=It was not possible to revert ignored updates from {} action.ignore_updates_reverse.success=Updates from {} will be displayed again from now on action.info.tooltip=Clicca qui per vedere informazioni su questa applicazione +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Action not allowed action.request_reboot.title=System restart action.reset=Restore @@ -112,49 +113,100 @@ cancel=cancella categories=categorie category=categoria category.2dgraphics=Grafica 2D +category.3dgraphics=Grafica 3D +category.accessibility=accessibility category.acessories=acessories +category.actiongame=action game category.admin=admin category.administration=amministrazione +category.adult=adult +category.adventuregame=adventure game category.amusement=amusement category.application=application category.arcadegame=Gioco arcade category.archiving=archiving category.art=art +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=audio -category.audiovideo=audio / video +category.audiovideo=audio/video +category.audiovideoediting=audio/video editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=books category.browser=browser +category.building=building +category.calendar=calendar category.calculator=calculator +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=cloud category.communication=comunicazione category.compression=compression +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=core category.database=database category.databases=databases +category.datavisualization=data visualization +category.debugger=debugger category.design=design category.desktopsettings=impostazioni del desktop category.devel=development category.development=development +category.dialup=dial-up category.dictionary=dictionary +category.discburning=disc burning category.doc=documentation +category.documentation=documentation +category.economy=economy category.education=education +category.electricity=electricity +category.electronics=electronics category.emulator=emulatore +category.engineering=engineering category.entertainment=entertainment +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=trasferimento di file category.finance=finance category.fitness=fitness +category.flowchart=flow chart category.fonts=fonts category.game=game category.games=games +category.geography=geography +category.geology=geology +category.geoscience=geoscience category.graphics=graphics +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.health=health +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=messaggistica +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=library category.library=library +category.literature=literature category.localization=localization +category.logicgame=logic game +category.maps=maps category.math=math category.media=media +category.medicalsoftware=medical category.messaging=messaggistica +category.mixer=mixer category.monitor=monitoring category.movie=movie category.movies=movies @@ -164,37 +216,67 @@ category.network=network category.networks=networks category.news=news category.none=None +category.numericalanalysis=numerical analysis category.office=office category.packagemanager=gestore pacchetti +category.parallelcomputing=parallel computing category.personalisation=personalization category.photo=photo category.photography=photo -category.player=giocatore +category.physics=physics +category.player=lettore category.presentation=presentation +category.printing=printing category.productivity=productivity +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=reference +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=science +category.screensaver=screensaver category.security=security +category.sequencer=sequencer category.server=server category.settings=impostazioni +category.shooter=shooter game +category.simulation=simulation game category.social=social category.sound=sound +category.spirituality=spirituality +category.sports=sport category.sportsgame=gioco di sport +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=system +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=terminale category.text editor=text editor category.texteditor=editor di testo +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Updates ignored category.utilities=utilities category.utility=utility category.vectorgraphics=grafica vettoriale category.video=video +category.videoconference=video conference category.videoeditor=video editor category.viewer=viewer category.weather=weather category.web=web category.webbrowser=browser category.webdevelopment=sviluppo web +category.wordprocessor=word processor change=Cambia clean=pulire close=vicino @@ -345,11 +427,13 @@ manage_window.bt_themes.tip=Click here to choose a theme manage_window.bt_themes.option.invalid=Invalid manage_window.checkbox.only_apps=Apps manage_window.checkbox.only_installed=Installate +manage_window.checkbox.only_verified=Verificati manage_window.checkbox.show_details=Mostra dettagli manage_window.columns.installed=Installed manage_window.columns.latest_version=Ultima Versione manage_window.columns.update=Upgrade ? manage_window.info.no_info=There is no information available for {} +manage_window.info.open_url=Aprire manage_window.label.apps_displayed.tip=Applications displayed / available manage_window.label.updates=Aggiornamenti manage_window.name_filter.button_tooltip=Click here to filter apps by name diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 17e898f70..7536607b3 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -66,6 +66,7 @@ action.ignore_updates.success=As atualizações de {} serão ignoradas a partir action.ignore_updates_reverse.fail=Não foi possível reverter as atualizações ignoradas de {} action.ignore_updates_reverse.success=As atualizações de {} serão exibidas novamente a partir de agora action.info.tooltip=Clique aqui para ver informações sobre este aplicativo +action.install.unverified.warning=Este app não foi verificado pelos mantenedores desse sistema ou uma fonte confiável. Confira suas informações antes de prosseguir. action.not_allowed=Ação não permitida action.request_reboot.title=Reinicialização de sistema action.reset=Restaurar @@ -111,49 +112,100 @@ bt.not_now=Agora não cancel=cancelar categories=categorias category.2dgraphics=gŕaficos 2d +category.3dgraphics=gŕaficos 3d +category.accessibility=acessibilidade category.acessories=acessórios +category.actiongame=jogo de ação category.admin=administrador category.administration=administração +category.adult=adulto +category.adventuregame=jogo de aventura category.amusement=diversão category.application=aplicação category.arcadegame=jogo arcade category.archiving=arquivamento category.art=arte +category.artificialintelligence=inteligência artificial +category.astronomy=astronomia category.audio=áudio -category.audiovideo=áudio / vídeo +category.audiovideo=áudio/vídeo +category.audiovideoediting=edição de áudio/video +category.biology=biologia +category.blocksgame=jogo de blocos +category.boardgame=jogo de tabuleiro category.books=livros category.browser=navegador +category.building=compilação +category.calendar=calendário category.calculator=calculadora +category.cardgame=jogo de cartas +category.chart=gráfico +category.chemistry=química +category.clock=relógio category.cloud=nuvem category.communication=comunicação category.compression=compressão +category.computerscience=ciências da computação +category.consoleonly=somente console +category.construction=construção +category.contactmanagement=contatos category.core=núcleo category.database=banco de dados category.databases=bancos de dados +category.datavisualization=visualização de dados +category.debugger=depurador category.design=desenho category.desktopsettings=configurações category.devel=desenvolvimento category.development=desenvolvimento +category.dialup=discagem category.dictionary=dicionário +category.discburning=gravador de disco category.doc=documentação +category.documentation=documentação +category.economy=economia category.education=educação +category.electricity=eletricidade +category.electronics=electrônica category.emulator=emulador +category.engineering=engenharia category.entertainment=entretenimento +category.feed=feed +category.filemanager=gerenciador de arquivos +category.filesystem=sistema de arquivos +category.filetools=ferramenta de arquivo category.filetransfer=transferência de arquivos category.finance=finanças category.fitness=fitness +category.flowchart=diagrama de fluxo category.fonts=fontes category.game=jogo category.games=jogos +category.geography=geografia +category.geology=geologia +category.geoscience=geociências category.graphics=gráficos +category.hamradio=rádio amador +category.hardwaresettings=configuração de hardware category.health=saúde +category.history=história +category.humanities=humanidades +category.imageprocessing=processamento de imagem category.instantmessaging=mensagem +category.ircclient=IRC +category.kidsgame=jogo infantil +category.languages=idiomas category.libs=biblioteca category.library=biblioteca +category.literature=literatura category.localization=localização +category.logicgame=jogo de lógica +category.maps=mapas category.math=matemática category.media=mídia +category.medicalsoftware=médico category.messaging=mensagem +category.mixer=mixer category.monitor=monitoria category.movie=filme category.movies=filmes @@ -163,37 +215,68 @@ category.network=rede category.networks=redes category.news=notícias category.none=Nenhuma +category.numericalanalysis=análise numérica category.office=escritório category.packagemanager=gerenciador de pacotes +category.parallelcomputing=computação paralela category.personalisation=personalização category.photo=fotografia category.photography=fotografia -category.player=jogador +category.physics=física +category.player=tocador category.presentation=apresentação +category.printing=impressão category.productivity=produtividade +category.profiling=performance +category.projectmanagement=projetos +category.publishing=publicação +category.rastergraphics=gráficos rasterizados +category.recorder=gravador +category.remoteaccess=acesso remoto category.reference=referência +category.revisioncontrol=controle de versão +category.robotics=robótica +category.roleplaying=jogo de RPG +category.scanning=digitalização +category.scanning=scanning category.science=ciências +category.screensaver=protetor de tela category.security=segurança +category.sequencer=sequenciador category.server=servidor category.settings=configuração +category.shooter=jogo de tiro +category.simulation=jogo de simulação category.social=social category.sound=som +category.spirituality=espiritualidade +category.sports=esportes category.sportsgame=jogo de esportes +category.spreadsheet=planilha +category.strategygame=jogo de estratégia category.system=sistema +category.telephony=telefonia +category.telephonytools=ferammentas de telefonia category.terminalemulator=terminal category.text editor=editor de texto category.texteditor=editor de texto +category.texttools=ferramentas de texto +category.translation=tradução +category.trayicon=ícone de bandeja +category.tuner=sintonizador category.updates_ignored=Atualizações ignoradas category.utilities=utilidades category.utility=utilidade category.vectorgraphics=gráficos vetoriais category.video=vídeo +category.videoconference=videoconferência category.videoeditor=edito de vídeo category.viewer=visualizador category.weather=tempo category.web=web category.webbrowser=navegador category.webdevelopment=desenvolvimento web +category.wordprocessor=editor de texto category=categoria change=alterar clean=limpar @@ -344,11 +427,13 @@ manage_window.bt_themes.tip=Clique aqui para escolher um tema manage_window.bt_themes.option.invalid=Inválido manage_window.checkbox.only_apps=Aplicativos manage_window.checkbox.only_installed=Instalados +manage_window.checkbox.only_verified=Verificados manage_window.checkbox.show_details=Mostrar detalhes manage_window.columns.installed=Instalado manage_window.columns.latest_version=Última Versão manage_window.columns.update=Atualizar ? manage_window.info.no_info=Não há informação disponível para {} +manage_window.info.open_url=Abrir manage_window.label.apps_displayed.tip=Aplicações exibidas / disponíveis manage_window.label.updates=Atualizações manage_window.name_filter.button_tooltip=Clique aqui para filtrar aplicativos por nome diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 3ab9e2d98..af9c4da69 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -66,6 +66,7 @@ action.ignore_updates.success=Обновления от {} теперь буду action.ignore_updates_reverse.fail=Не удалось вернуть игнорируемые обновления из {} action.ignore_updates_reverse.success=Теперь обновления из {} будут отображаться снова action.info.tooltip=Нажмите здесь, чтобы посмотреть информацию об этом приложении +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Действие не допускается action.request_reboot.title=Перезапуск системы action.reset=Восстановить @@ -111,50 +112,99 @@ bt.not_now=Не сейчас cancel=Отмена categories=категории category.2dgraphics=2D-графика +category.3dgraphics=3D-графика +category.accessibility=accessibility category.acessories=Аксессуары +category.actiongame=action game category.admin=Админ category.administration=Администрирование +category.adult=adult +category.adventuregame=adventure game category.amusement=Развлечения category.application=Приложение category.arcadegame=Аркада category.archiving=Архивирование category.art=Искусство +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=Аудио category.audiovideo=Аудио/Видео +category.audiovideoediting=Аудио/Видео editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=Книги category.browser=Браузер +category.building=building +category.calendar=calendar category.calculator=Калькулятор +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=Облако category.communication=Общение category.compression=Сжатие +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction category.core=Ядро category.database=База данных category.databases=Базы данных +category.datavisualization=data visualization +category.debugger=debugger category.design=Дизайн category.desktopsettings=Настройки рабочего стола category.devel=Разработка category.development=Разработка +category.dialup=dial-up category.dictionary=Словарь +category.discburning=disc burning category.doc=Документация +category.documentation=Документация +category.economy=economy category.education=Образование +category.electricity=electricity +category.electronics=electronics category.emulator=Эмуляторы +category.engineering=engineering category.entertainment=Развлечения +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=Передача файлов category.finance=Финансы category.fitness=Фитнес +category.flowchart=flow chart category.fonts=Шрифты category.game=Игра category.games=Игры +category.geography=geography +category.geology=geology +category.geoscience=geoscience category.graphics=Графика +category.hamradio=ham radio category.hardwaresettings=Оборудование category.health=Здоровье +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=Мессенджер +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=Библиотека category.library=Библиотека +category.literature=literature category.localization=Локализация +category.logicgame=logic game +category.maps=maps category.math=Математика category.media=Медиа +category.medicalsoftware=medical category.messaging=Сообщения +category.mixer=mixer category.monitor=Мониторинг category.movie=Фильм category.movies=Фильмы @@ -164,38 +214,67 @@ category.network=Сеть category.networks=Сети category.news=Новости category.none=Нет +category.numericalanalysis=numerical analysis category.office=Офис category.packagemanager=Пакетный менеджер +category.parallelcomputing=parallel computing category.personalisation=Персонализация category.photo=Фото category.photography=Фото -category.player=Проигрыватель +category.physics=physics category.player=Проигрыватель category.presentation=Презентация +category.printing=printing category.productivity=Производительность +category.profiling=profiling +category.projectmanagement=projects +category.publishing=publishing +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=Справка +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=Наука +category.screensaver=screensaver category.security=Безопасность +category.sequencer=sequencer category.server=Сервер category.settings=Настройки +category.shooter=shooter game +category.simulation=simulation game category.social=Социальные category.sound=Звук +category.spirituality=spirituality +category.sports=sports category.sportsgame=Спортивная игра +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=Система +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=Эмулятор терминала category.text editor=Текстовый редактор category.texteditor=Текстовый редактор +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Обновления игнорируются category.utilities=Утилиты category.utility=Утилита category.vectorgraphics=Векторная графика category.video=Видео +category.videoconference=video conference category.videoeditor=Видеоредактор category.viewer=Просмотр category.weather=Погода category.web=Сеть category.webbrowser=Браузер category.webdevelopment=Web-разработка +category.wordprocessor=word processor category=Категория change=Изменить clean=Очистка @@ -346,11 +425,13 @@ manage_window.bt_themes.tip=Нажмите здесь, чтобы выбрать manage_window.bt_themes.option.invalid=Недопустимый manage_window.checkbox.only_apps=Приложения manage_window.checkbox.only_installed=Установленные +manage_window.checkbox.only_verified=Проверено manage_window.checkbox.show_details=Показать детали manage_window.columns.installed=Установленно manage_window.columns.latest_version=Последняя версия manage_window.columns.update=Обновить? manage_window.info.no_info=Информация не доступна для {} +manage_window.info.open_url=Открыть manage_window.label.apps_displayed.tip=Отображаемые / доступные приложения manage_window.label.updates=Обновления manage_window.name_filter.button_tooltip=Нажмите здесь, чтобы отфильтровать приложения по названию diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr index 750b26e5a..7b14c93d5 100644 --- a/bauh/view/resources/locale/tr +++ b/bauh/view/resources/locale/tr @@ -66,6 +66,7 @@ action.ignore_updates.success=Updates from {} will be ignored from now on action.ignore_updates_reverse.fail=It was not possible to revert ignored updates from {} action.ignore_updates_reverse.success=Updates from {} will be displayed again from now on action.info.tooltip=Uygulama bilgilerini görmek için buraya tıkla +action.install.unverified.warning=This app has not been verified by this system maintainers or a trusted source. Check its information before proceeding. action.not_allowed=Eyleme izin verilmiyor action.request_reboot.title=Sistemi yeniden başlat action.reset=Restore @@ -111,49 +112,100 @@ bt.not_now=Şimdi değil cancel=vazgeç categories=Kategoriler category.2dgraphics=2d grafikler +category.3dgraphics=3d grafikler +category.accessibility=accessibility category.acessories=donatılar +category.actiongame=action game category.admin=admin category.administration=yönetim +category.adult=adult +category.adventuregame=adventure game category.amusement=eğlence category.application=application category.arcadegame=arcade oyunlar category.archiving=arşivleme category.art=sanat +category.artificialintelligence=artificial intelligence +category.astronomy=astronomy category.audio=ses -category.audiovideo=ses / video +category.audiovideo=ses/video +category.audiovideoediting=ses/video editing +category.biology=biology +category.blocksgame=blocks game +category.boardgame=board game category.books=kitaplar category.browser=tarayıcı +category.building=building +category.calendar=calendar category.calculator=hesap makinesi +category.cardgame=card game +category.chart=chart +category.chemistry=chemistry +category.clock=clock category.cloud=bulut category.communication=iletişim category.compression=sıkıştırma +category.computerscience=computer science +category.consoleonly=console only +category.construction=construction +category.contactmanagement=contacts category.core=core category.database=veritabanı category.databases=veritabanları +category.datavisualization=data visualization +category.debugger=debugger category.design=tasarım category.desktopsettings=masaüstü ayarları category.devel=gelişim category.development=gelişim +category.dialup=dial-up category.dictionary=dictionary +category.discburning=disc burning category.doc=documentation +category.documentation=documentation +category.economy=economy category.education=eğitim +category.electricity=electricity +category.electronics=electronics category.emulator=öykünücü +category.engineering=engineering category.entertainment=eğlence +category.feed=feed +category.filemanager=file manager +category.filesystem=file system +category.filetools=file tools category.filetransfer=dosya transferi category.finance=finans category.fitness=sağlık +category.flowchart=flow chart category.fonts=fontlar category.game=oyun category.games=oyunlar +category.geography=geography +category.geology=geology +category.geoscience=geoscience category.graphics=grafikler +category.hamradio=ham radio +category.hardwaresettings=hardware settings category.health=sağlık +category.history=history +category.humanities=humanities +category.imageprocessing=image processing category.instantmessaging=anlık mesajlaşma +category.ircclient=IRC +category.kidsgame=kids game +category.languages=languages category.libs=kütüphane category.library=kütüphane +category.literature=literature category.localization=localization +category.logicgame=logic game +category.maps=maps category.math=math category.media=medya +category.medicalsoftware=medical category.messaging=mesajlaşma +category.mixer=mixer category.monitor=monitoring category.movie=film category.movies=filmler @@ -163,37 +215,67 @@ category.network=ağ category.networks=ağlar category.news=haberler category.none=hiçbiri +category.numericalanalysis=numerical analysis category.office=ofis category.packagemanager=paket yönetici +category.parallelcomputing=parallel computing category.personalisation=kişiselleştirme category.photo=fotoğraf category.photography=fotoğraf +category.physics=physics category.player=oyuncu category.presentation=sunum +category.printing=printing category.productivity=verimlilik +category.projectmanagement=projects +category.publishing=publishing +category.profiling=profiling +category.rastergraphics=raster graphics +category.recorder=recorder +category.remoteaccess=remote access category.reference=referans +category.revisioncontrol=version control +category.robotics=robotics +category.roleplaying=RPG game +category.scanning=scanning category.science=bilim +category.screensaver=screensaver category.security=güvenlik +category.sequencer=sequencer category.server=sunucu category.settings=ayarlar +category.shooter=shooter game +category.simulation=simulation game category.social=sosyal category.sound=sound +category.spirituality=spirituality +category.sports=sports category.sportsgame=spor oyunları +category.spreadsheet=spreadsheet +category.strategygame=strategy game category.system=sistem +category.telephony=telephony +category.telephonytools=telephony tools category.terminalemulator=uçbirim category.text editor=metin düzenleyici category.texteditor=metin düzenleyici +category.texttools=text tools +category.translation=translation +category.trayicon=tray icon +category.tuner=tuner category.updates_ignored=Görmezden gelinen güncellemeler category.utilities=bileşenler category.utility=bileşenler category.vectorgraphics=vektör grafikler category.video=video +category.videoconference=video conference category.videoeditor=video düzenleyici category.viewer=gösterici category.weather=hava durumu category.web=web category.webbrowser=tarayıcı category.webdevelopment=web geliştirme +category.wordprocessor=word processor category=kategori change=değiştir clean=temizle @@ -343,11 +425,13 @@ manage_window.bt_themes.tip=Tema seçmek için burayı tıkla manage_window.bt_themes.option.invalid=Invalid manage_window.checkbox.only_apps=Uygulamalar manage_window.checkbox.only_installed=Yüklü +manage_window.checkbox.only_verified=Doğrulandı manage_window.checkbox.show_details=Detayları göster manage_window.columns.installed=Yüklü manage_window.columns.latest_version=Son sürüm manage_window.columns.update=Yükselt ? manage_window.info.no_info={} için bilgi yok +manage_window.info.open_url=Açmak manage_window.label.apps_displayed.tip=Görüntülenen uygulamalar / mevcut olanlar manage_window.label.updates=Güncellemeler manage_window.name_filter.button_tooltip=Click here to filter apps by name diff --git a/bauh/view/resources/locale/tray/__init__.py b/bauh/view/resources/locale/tray/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/darcula/__init__.py b/bauh/view/resources/style/darcula/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/darcula/darcula.qss b/bauh/view/resources/style/darcula/darcula.qss index cb948165d..ad703a5cb 100644 --- a/bauh/view/resources/style/darcula/darcula.qss +++ b/bauh/view/resources/style/darcula/darcula.qss @@ -424,6 +424,10 @@ QLabel#confirm_dialog_icon { qproperty-pixmap: url("@style_dir/img/question.svg"); } +QLabel#confirm_dialog_icon[type = "warning"] { + qproperty-pixmap: url("@style_dir/img/warning.svg"); +} + FormQt IconButton#clean_field { qproperty-icon: url("@style_dir/img/clean.svg"); } diff --git a/bauh/view/resources/style/darcula/img/__init__.py b/bauh/view/resources/style/darcula/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/default/__init__.py b/bauh/view/resources/style/default/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/knight/__init__.py b/bauh/view/resources/style/knight/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/knight/img/__init__.py b/bauh/view/resources/style/knight/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/knight/img/warning.svg b/bauh/view/resources/style/knight/img/warning.svg new file mode 100644 index 000000000..31c505ed6 --- /dev/null +++ b/bauh/view/resources/style/knight/img/warning.svg @@ -0,0 +1,122 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauh/view/resources/style/knight/knight.qss b/bauh/view/resources/style/knight/knight.qss index cf42fe6b0..2ef532315 100644 --- a/bauh/view/resources/style/knight/knight.qss +++ b/bauh/view/resources/style/knight/knight.qss @@ -417,6 +417,10 @@ QLabel#confirm_dialog_icon { qproperty-pixmap: url("@style_dir/img/help.svg"); } +QLabel#confirm_dialog_icon[type = "warning"] { + qproperty-pixmap: url("@style_dir/img/warning.svg"); +} + FormQt IconButton#clean_field { qproperty-icon: url("@style_dir/img/clean.svg"); } diff --git a/bauh/view/resources/style/light/__init__.py b/bauh/view/resources/style/light/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/light/img/__init__.py b/bauh/view/resources/style/light/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/light/light.qss b/bauh/view/resources/style/light/light.qss index 18e3f2240..dc78d424d 100644 --- a/bauh/view/resources/style/light/light.qss +++ b/bauh/view/resources/style/light/light.qss @@ -83,6 +83,10 @@ QLabel#confirm_dialog_icon { qproperty-pixmap: url("@style_dir/img/question.svg"); } +QLabel#confirm_dialog_icon[type = "warning"] { + qproperty-pixmap: url("@style_dir/img/warning.svg"); +} + QPlainTextEdit[console = 'true'] { background: @console.background.color; color: @console.font.color; diff --git a/bauh/view/resources/style/sublime/__init__.py b/bauh/view/resources/style/sublime/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/sublime/img/__init__.py b/bauh/view/resources/style/sublime/img/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/bauh/view/resources/style/sublime/sublime.qss b/bauh/view/resources/style/sublime/sublime.qss index df3e63535..41dc257b5 100644 --- a/bauh/view/resources/style/sublime/sublime.qss +++ b/bauh/view/resources/style/sublime/sublime.qss @@ -131,6 +131,10 @@ QLabel#confirm_dialog_icon { qproperty-pixmap: url("@style_dir/img/question.svg"); } +QLabel#confirm_dialog_icon[type = "warning"] { + qproperty-pixmap: url("@style_dir/img/warning.svg"); +} + QPlainTextEdit { background: @texteditor.background.color; color: @texteditor.font.color; diff --git a/bauh/view/util/cache.py b/bauh/view/util/cache.py index 09b8cfdd1..d5e26251e 100644 --- a/bauh/view/util/cache.py +++ b/bauh/view/util/cache.py @@ -1,6 +1,5 @@ import datetime -import time -from threading import Lock, Thread +from threading import Lock from typing import Optional from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory @@ -76,41 +75,14 @@ def clean_expired(self): self.get(key) -class CacheCleaner(Thread): - - def __init__(self, check_interval: int = 15): - super(CacheCleaner, self).__init__(daemon=True) - self.caches = [] - self.check_interval = check_interval - - def register(self, cache: MemoryCache): - if cache.is_enabled(): - self.caches.append(cache) - - def run(self): - if self.caches: - while True: - for cache in self.caches: - cache.clean_expired() - - time.sleep(self.check_interval) - - class DefaultMemoryCacheFactory(MemoryCacheFactory): - def __init__(self, expiration_time: int, cleaner: CacheCleaner = None): + def __init__(self, expiration_time: int): """ :param expiration_time: default expiration time for all instantiated caches - :param cleaner """ super(DefaultMemoryCacheFactory, self).__init__() self.expiration_time = expiration_time - self.cleaner = cleaner def new(self, expiration: Optional[int] = None) -> MemoryCache: - instance = DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) - - if self.cleaner: - self.cleaner.register(instance) - - return instance + return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) diff --git a/linux_dist/appimage/AppImageBuilder.yml b/linux_dist/appimage/AppImageBuilder.yml index 58fed2c02..f0b5ad5ce 100755 --- a/linux_dist/appimage/AppImageBuilder.yml +++ b/linux_dist/appimage/AppImageBuilder.yml @@ -1,13 +1,14 @@ version: 1 script: - rm -rf AppDir appimage-builder-cache *.AppImage *.zsync |true - - mkdir -p AppDir/usr - mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps - mkdir -p AppDir/usr/share/applications - wget https://github.com/vinifmor/bauh/archive/${BAUH_VERSION}.tar.gz || exit 1 - - BAUH_SETUP_NO_REQS=1 pip3 install ${BAUH_VERSION}.tar.gz --prefix=/usr --root=AppDir || exit 1 - tar -xf ${BAUH_VERSION}.tar.gz || exit 1 - cd bauh-${BAUH_VERSION} || exit 1 + - rm pyproject.toml || exit 1 # not using the new project file definition for now + - BAUH_SETUP_NO_REQS=1 pip3 install . --prefix=/usr --root=../AppDir || exit 1 + - test -e ../AppDir/usr/bin/bauh || exit 1 - cp bauh/view/resources/img/logo.svg ../AppDir/usr/share/icons/hicolor/scalable/apps/bauh.svg || exit 1 - cp bauh/desktop/bauh.desktop ../AppDir/usr/share/applications || exit 1 @@ -27,8 +28,8 @@ AppDir: apt: arch: amd64 sources: - - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ focal main restricted universe multiverse' - key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: 'deb http://deb.debian.org/debian/ bullseye main contrib non-free' + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xa4285295fc7b1a81600062a9605c66f00d6c9793' include: - python3 @@ -39,9 +40,7 @@ AppDir: - python3-lxml - python3-bs4 - sqlite3 - - wget - # - libfreetype6 - # - libfontconfig1 + - xdg-utils exclude: - dpkg - apt @@ -51,20 +50,27 @@ AppDir: - python3-distutils after_bundle: - - pip3 install pyqt5 --prefix=/usr --root=AppDir || exit 1 - - cd AppDir/usr/lib/python3.8/site-packages/PyQt5/Qt5/plugins || exit 1 - - rm -rf audio gamepad gamepads geoservices printsupport sceneparsers sensorgestures sensors sqldrivers texttospeech webview || exit 1 + - pip3 install pyqt5==5.15.10 --prefix=/usr --root=AppDir || exit 1 + - rm -rf AppDir/usr/share/doc || exit 1 + - cd AppDir/usr/lib/python3.9/site-packages/PyQt5/Qt5/plugins || exit 1 + - rm -rf audio gamepad gamepads geoservices printsupport sceneparsers sensorgestures sensors sqldrivers texttospeech webview mediaservice playlistformats || exit 1 - cd ../lib/ || exit 1 - rm libQt5Bluetooth.so.5 libQt5Designer.so.5 libQt5Multimedia.so.5 libQt5MultimediaGstTools.so.5 libQt5MultimediaWidgets.so.5 || exit 1 - rm libQt5Quick3D.so.5 libQt5Quick3DAssetImport.so.5 libQt5Quick3DRender.so.5 libQt5Quick3DRuntimeRender.so.5 libQt5QuickTest.so.5 || exit 1 - rm libQt5Quick3DUtils.so.5 libQt5PrintSupport.so.5 libQt5SerialPort.so.5 libQt5Sql.so.5 libQt5Sensors.so.5 libQt5Test.so.5 libQt5WebView.so.5 || exit 1 - rm libQt5Quick.so.5 libQt5Location.so.5 libQt5QuickTemplates2.so.5 || exit 1 - + - cd ../qml || exit 1 + - rm -rf QtBluetooth QtMultimedia QtQuick QtQuick.2 QtQuick3D QtRemoteObjects QtSensors QtTest + - cd ../../ || exit 1 + - rm QtBluetooth.abi3.so QtMultimedia.abi3.so QtMultimediaWidgets.abi3.so QtPrintSupport.abi3.so QtQuick.abi3.so QtQuick3D.abi3.so QtQuickWidgets.abi3.so QtRemoteObjects.abi3.so QtSensors.abi3.so QtSerialPort.abi3.so QtSql.abi3.so QtTest.abi3.so QtTextToSpeech.abi3.so QtWebSockets.abi3.so|| exit 1 + - rm QtBluetooth.pyi QtMultimedia.pyi QtMultimediaWidgets.pyi QtPrintSupport.pyi QtQuick3D.pyi QtQuickWidgets.pyi QtSensors.pyi QtSerialPort.pyi QtSql.pyi + - cd bindings || exit 1 + - rm -rf QtBluetooth QtMultimedia QtMultimediaWidgets QtPrintSupport QtSensors QtSerialPort QtTextToSpeech QtQuick QtQuick3D QtQuickWidgets QtSql QtTest || exit 1 runtime: version: "v1.2.5" env: PYTHONHOME: '${APPDIR}/usr' - PYTHONPATH: '${APPDIR}/usr/lib/python3.8/site-packages' + PYTHONPATH: '${APPDIR}/usr/lib/python3.9/site-packages' test: fedora: diff --git a/linux_dist/appimage/Dockerfile b/linux_dist/appimage/Dockerfile index a8d9f40ea..8c77f71be 100644 --- a/linux_dist/appimage/Dockerfile +++ b/linux_dist/appimage/Dockerfile @@ -1,12 +1,10 @@ -FROM ubuntu:20.04 +FROM debian:bullseye-slim -ARG bauh_commit -ENV BAUH_VERSION=$bauh_commit ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ apt-get upgrade -y && \ - apt-get install python3-pip python3-setuptools python3-wheel wget fuse binutils coreutils desktop-file-utils fakeroot patchelf squashfs-tools strace zsync libgdk-pixbuf2.0-dev gtk-update-icon-cache -y && \ + apt-get install --no-install-recommends python3-pip python3-setuptools python3-wheel wget fuse binutils coreutils desktop-file-utils fakeroot patchelf squashfs-tools strace zsync libgdk-pixbuf2.0-dev gtk-update-icon-cache file -y && \ mkdir /build && cd /build && \ wget https://github.com/AppImageCrafters/appimage-builder/releases/download/v0.9.2/appimage-builder-0.9.2-35e3eab-x86_64.AppImage -O appimage-builder && \ wget https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -O appimage-tool && \ @@ -17,6 +15,4 @@ RUN apt-get update -y && \ WORKDIR /build -COPY AppImageBuilder.yml /build - CMD [ "appimage-builder", "--skip-tests"] diff --git a/linux_dist/appimage/build.sh b/linux_dist/appimage/build.sh index b871db60e..ffe65632a 100755 --- a/linux_dist/appimage/build.sh +++ b/linux_dist/appimage/build.sh @@ -1,3 +1,6 @@ #!/bin/bash -docker build -t bauh-appimage --build-arg bauh_commit=$BAUH_COMMIT . && \ -docker run --cap-add=SYS_ADMIN --device /dev/fuse --mount type=bind,source="$(pwd)",target=/build bauh-appimage +set -Ceufox pipefail + +docker build -t bauh-appimage . +docker run -e BAUH_VERSION=$BAUH_VERSION -v ./AppImageBuilder.yml:/build/AppImageBuilder.yml --rm --cap-add=SYS_ADMIN --device /dev/fuse --mount type=bind,source="$(pwd)",target=/build bauh-appimage +# volume required to run tests: -v /var/run/docker.sock:/var/run/docker.sock diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..11897aa94 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "bauh" +description = "Graphical interface to manage Linux applications (AppImage, Arch / AUR, Flatpak, Snap and Web)" +license = {file = "LICENSE"} +requires-python = ">=3.6" +dynamic = ["version"] +readme = "README.md" +authors = [{name = "Vinicius Moreira", email = "vinicius_fmoreira@hotmail.com"}] +classifiers = [ + 'Topic :: Utilities', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11' +] + +dependencies = [ + "pyqt5 >= 5.12", + "requests >= 2.18", + "colorama >= 0.3.8", + "pyyaml >= 3.13", + "python-dateutil >= 2.7" +] + +[project.optional-dependencies] +web = [ + "lxml >= 4.2.0", + "beautifulsoup4 >= 4.7.0" +] + +[project.scripts] +bauh = "bauh.app:main" +bauh-tray = "bauh.app:tray" +bauh-cli = "bauh.cli.app:main" + +[project.urls] +Repository = "https://github.com/vinifmor/bauh" + +[tool.setuptools] +license-files = ["LICENSE"] + +[tool.setuptools.dynamic] +version = {attr = "bauh.__version__"} + +[tool.setuptools.packages.find] +exclude = ["tests.*", "tests"] diff --git a/setup.cfg b/setup.cfg index b88034e41..08aedd7e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file = README.md +description_file = README.md diff --git a/setup.py b/setup.py index 2f3cbae46..744e2e41f 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ "Graphical interface to manage Linux applications (AppImage, Arch / AUR, Flatpak, Snap and Web)" ) -AUTHOR = "bauh developers" -AUTHOR_EMAIL = "bauh4linux@gmail.com" +AUTHOR = "Vinicius Moreira" +AUTHOR_EMAIL = "vinicius_fmoreira@hotmail.com" NAME = 'bauh' URL = "https://github.com/vinifmor/" + NAME