From 01a60ea68612e10f69149318f975afc70756fc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?= Date: Mon, 13 Apr 2020 11:49:28 -0300 Subject: [PATCH] 0.9.0 --- CHANGELOG.md | 89 + MANIFEST.in | 1 + README.md | 162 +- aur/bauh-tray | 2 - aur/bauh.desktop | 17 - aur/bauh_tray.desktop | 17 - aur/copy_icons.py | 14 - aur/icons/128x128.png | Bin 6580 -> 0 bytes aur/icons/12x12.png | Bin 889 -> 0 bytes aur/icons/16x16.png | Bin 1011 -> 0 bytes aur/icons/24x24.png | Bin 1433 -> 0 bytes aur/icons/256x256.png | Bin 9831 -> 0 bytes aur/icons/32x32.png | Bin 1667 -> 0 bytes aur/icons/48x48.png | Bin 2745 -> 0 bytes aur/icons/512x512.png | Bin 22325 -> 0 bytes aur/icons/64x64.png | Bin 2983 -> 0 bytes aur/icons/8x8.png | Bin 726 -> 0 bytes aur/icons/96x96.png | Bin 4429 -> 0 bytes bauh/__init__.py | 3 +- bauh/api/abstract/context.py | 6 +- bauh/api/abstract/controller.py | 74 +- bauh/api/abstract/handler.py | 40 +- bauh/api/abstract/model.py | 47 +- bauh/api/abstract/view.py | 13 +- bauh/api/constants.py | 6 +- bauh/api/http.py | 15 +- bauh/app.py | 109 +- bauh/app_args.py | 18 +- bauh/cli/__init__.py | 3 + bauh/cli/app.py | 56 + bauh/cli/cli_args.py | 15 + bauh/cli/controller.py | 30 + bauh/commons/category.py | 15 +- bauh/commons/html.py | 4 + bauh/commons/internet.py | 21 +- bauh/commons/system.py | 12 +- bauh/commons/util.py | 17 + bauh/context.py | 46 + bauh/desktop/bauh.desktop | 17 + bauh/desktop/bauh_tray.desktop | 17 + bauh/gems/appimage/__init__.py | 7 +- bauh/gems/appimage/controller.py | 202 +- bauh/gems/appimage/model.py | 41 +- bauh/gems/appimage/resources/img/refresh.svg | 146 ++ bauh/gems/appimage/resources/locale/ca | 53 +- bauh/gems/appimage/resources/locale/de | 53 +- bauh/gems/appimage/resources/locale/en | 53 +- bauh/gems/appimage/resources/locale/es | 53 +- bauh/gems/appimage/resources/locale/it | 52 +- bauh/gems/appimage/resources/locale/pt | 53 +- bauh/gems/appimage/resources/locale/ru | 51 +- bauh/gems/appimage/worker.py | 32 +- bauh/gems/arch/__init__.py | 20 +- bauh/gems/arch/aur.py | 90 +- bauh/gems/arch/config.py | 9 +- bauh/gems/arch/confirmation.py | 81 +- bauh/gems/arch/controller.py | 1955 ++++++++++++----- bauh/gems/arch/cpu_manager.py | 38 + bauh/gems/arch/database.py | 52 + bauh/gems/arch/depedencies.py | 130 -- bauh/gems/arch/dependencies.py | 466 ++++ bauh/gems/arch/disk.py | 124 +- bauh/gems/arch/mapper.py | 2 +- bauh/gems/arch/message.py | 25 +- bauh/gems/arch/mirrors.py | 42 + bauh/gems/arch/model.py | 35 +- bauh/gems/arch/output.py | 109 + bauh/gems/arch/pacman.py | 593 ++++- .../resources/img/{mirror.svg => repo.svg} | 2 +- bauh/gems/arch/resources/locale/ca | 306 ++- bauh/gems/arch/resources/locale/de | 306 ++- bauh/gems/arch/resources/locale/en | 306 ++- bauh/gems/arch/resources/locale/es | 306 ++- bauh/gems/arch/resources/locale/it | 274 ++- bauh/gems/arch/resources/locale/pt | 306 ++- bauh/gems/arch/resources/locale/ru | 306 ++- bauh/gems/arch/sorting.py | 148 ++ bauh/gems/arch/updates.py | 425 ++++ bauh/gems/arch/worker.py | 222 +- bauh/gems/flatpak/controller.py | 78 +- bauh/gems/flatpak/flatpak.py | 28 +- bauh/gems/flatpak/model.py | 25 +- bauh/gems/flatpak/resources/locale/ca | 101 +- bauh/gems/flatpak/resources/locale/de | 100 +- bauh/gems/flatpak/resources/locale/en | 100 +- bauh/gems/flatpak/resources/locale/es | 101 +- bauh/gems/flatpak/resources/locale/it | 80 +- bauh/gems/flatpak/resources/locale/pt | 101 +- bauh/gems/flatpak/resources/locale/ru | 100 +- bauh/gems/snap/__init__.py | 5 + bauh/gems/snap/controller.py | 31 +- bauh/gems/snap/model.py | 17 +- bauh/gems/snap/resources/locale/ca | 22 +- bauh/gems/snap/resources/locale/de | 22 +- bauh/gems/snap/resources/locale/en | 22 +- bauh/gems/snap/resources/locale/es | 22 +- bauh/gems/snap/resources/locale/it | 22 +- bauh/gems/snap/resources/locale/pt | 22 +- bauh/gems/snap/resources/locale/ru | 22 +- bauh/gems/web/__init__.py | 7 +- bauh/gems/web/controller.py | 76 +- bauh/gems/web/environment.py | 19 +- bauh/gems/web/model.py | 3 + bauh/gems/web/resources/locale/ca | 75 + bauh/gems/web/resources/locale/en | 108 +- bauh/gems/web/resources/locale/es | 106 +- bauh/gems/web/resources/locale/it | 75 + bauh/gems/web/resources/locale/pt | 106 +- bauh/gems/web/resources/locale/ru | 114 +- bauh/gems/web/worker.py | 19 +- bauh/manage.py | 75 + bauh/tray.py | 14 + bauh/view/core/config.py | 34 +- bauh/view/core/controller.py | 260 ++- bauh/view/core/settings.py | 128 +- bauh/view/core/timeshift.py | 13 + bauh/view/core/tray_client.py | 12 + bauh/view/qt/about.py | 15 +- bauh/view/qt/apps_table.py | 104 +- bauh/view/qt/commons.py | 2 +- bauh/view/qt/components.py | 48 +- bauh/view/qt/confirmation.py | 5 +- bauh/view/qt/gem_selector.py | 2 +- bauh/view/qt/history.py | 5 +- bauh/view/qt/info.py | 2 +- bauh/view/qt/prepare.py | 311 +++ bauh/view/qt/root.py | 15 +- bauh/view/qt/settings.py | 41 +- bauh/view/qt/systray.py | 115 +- bauh/view/qt/thread.py | 605 +++-- bauh/view/qt/view_model.py | 18 +- bauh/view/qt/window.py | 322 +-- bauh/view/resources/img/custom_actions.svg | 61 + bauh/view/resources/img/exclamation.svg | 140 +- bauh/view/resources/img/logo.svg | 830 ++++--- bauh/view/resources/img/logo_update.svg | 965 ++++---- bauh/view/resources/locale/about/ca | 9 + bauh/view/resources/locale/about/de | 9 + bauh/view/resources/locale/about/en | 9 + bauh/view/resources/locale/about/es | 9 + bauh/view/resources/locale/about/it | 9 + bauh/view/resources/locale/about/pt | 9 + bauh/view/resources/locale/about/ru | 9 + bauh/view/resources/locale/ca | 607 ++--- bauh/view/resources/locale/de | 561 +++-- bauh/view/resources/locale/en | 590 +++-- bauh/view/resources/locale/es | 602 ++--- bauh/view/resources/locale/it | 564 +++-- bauh/view/resources/locale/pt | 634 +++--- bauh/view/resources/locale/ru | 560 +++-- bauh/view/resources/locale/tray/ca | 7 + bauh/view/resources/locale/tray/de | 7 + bauh/view/resources/locale/tray/en | 7 + bauh/view/resources/locale/tray/es | 7 + bauh/view/resources/locale/tray/it | 7 + bauh/view/resources/locale/tray/pt | 7 + bauh/view/resources/locale/tray/ru | 7 + bauh/view/util/cache.py | 7 +- bauh/view/util/disk.py | 67 +- bauh/view/util/translation.py | 6 +- bauh/view/util/util.py | 21 +- pictures/{aur => arch}/search.gif | Bin setup.py | 14 +- tests/gems/arch/test_controller.py | 168 -- tests/gems/arch/test_sorting.py | 205 ++ 165 files changed, 12778 insertions(+), 6171 deletions(-) delete mode 100644 aur/bauh-tray delete mode 100644 aur/bauh.desktop delete mode 100644 aur/bauh_tray.desktop delete mode 100644 aur/copy_icons.py delete mode 100644 aur/icons/128x128.png delete mode 100644 aur/icons/12x12.png delete mode 100644 aur/icons/16x16.png delete mode 100644 aur/icons/24x24.png delete mode 100644 aur/icons/256x256.png delete mode 100644 aur/icons/32x32.png delete mode 100644 aur/icons/48x48.png delete mode 100644 aur/icons/512x512.png delete mode 100644 aur/icons/64x64.png delete mode 100644 aur/icons/8x8.png delete mode 100644 aur/icons/96x96.png create mode 100644 bauh/cli/__init__.py create mode 100644 bauh/cli/app.py create mode 100644 bauh/cli/cli_args.py create mode 100644 bauh/cli/controller.py create mode 100644 bauh/context.py create mode 100644 bauh/desktop/bauh.desktop create mode 100644 bauh/desktop/bauh_tray.desktop create mode 100755 bauh/gems/appimage/resources/img/refresh.svg create mode 100644 bauh/gems/arch/cpu_manager.py create mode 100644 bauh/gems/arch/database.py delete mode 100644 bauh/gems/arch/depedencies.py create mode 100644 bauh/gems/arch/dependencies.py create mode 100644 bauh/gems/arch/mirrors.py create mode 100644 bauh/gems/arch/output.py rename bauh/gems/arch/resources/img/{mirror.svg => repo.svg} (98%) create mode 100644 bauh/gems/arch/sorting.py create mode 100644 bauh/gems/arch/updates.py create mode 100644 bauh/gems/web/resources/locale/ca create mode 100644 bauh/gems/web/resources/locale/it create mode 100644 bauh/manage.py create mode 100644 bauh/tray.py create mode 100644 bauh/view/core/timeshift.py create mode 100644 bauh/view/core/tray_client.py create mode 100644 bauh/view/qt/prepare.py create mode 100644 bauh/view/resources/img/custom_actions.svg mode change 100755 => 100644 bauh/view/resources/img/exclamation.svg mode change 100755 => 100644 bauh/view/resources/img/logo.svg mode change 100755 => 100644 bauh/view/resources/img/logo_update.svg create mode 100644 bauh/view/resources/locale/about/ca create mode 100644 bauh/view/resources/locale/about/de create mode 100644 bauh/view/resources/locale/about/en create mode 100644 bauh/view/resources/locale/about/es create mode 100644 bauh/view/resources/locale/about/it create mode 100644 bauh/view/resources/locale/about/pt create mode 100644 bauh/view/resources/locale/about/ru create mode 100644 bauh/view/resources/locale/tray/ca create mode 100644 bauh/view/resources/locale/tray/de create mode 100644 bauh/view/resources/locale/tray/en create mode 100644 bauh/view/resources/locale/tray/es create mode 100644 bauh/view/resources/locale/tray/it create mode 100644 bauh/view/resources/locale/tray/pt create mode 100644 bauh/view/resources/locale/tray/ru rename pictures/{aur => arch}/search.gif (100%) delete mode 100644 tests/gems/arch/test_controller.py create mode 100644 tests/gems/arch/test_sorting.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 512f8b21..6bf34179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,95 @@ 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.9.0] - 2020-04 +### Features +- Backup + - Timeshift integration ( if available on the system ): it can generate snapshots before installing, uninstalling, upgrading... + - you can enable / disable this feature via the settings file or UI. +- UI + - new initialization dialog showing tasks that need to be done before use + - new custom actions button ( displays specific action available for each packaging provider ) +- Arch + - support for packages from configured repositories ( search, install, upgrade and info. **History and downgrade are not supported yet** ) + - custom actions ( available through the new custom actions button ) + - **synchronize packages database**: synchronizes the database against the configured mirrors + - **refresh mirrors**: allows the user to define multiple mirrors locations and sort by the fastest + - **quick system upgrade**: it executes a default pacman upgrade ( `pacman -Syyu --noconfirm` ) + - **clean cache**: it cleans the pacman cache diretory ( default: `/var/cache/pacman/pkg` ) + - mirrors refreshing on startup ( **disabled by default**. Can be enabled on settings -> **refresh_mirrors_startup** ) + - new settings to enable / disable AUR and repository packages management: `aur` and `repositories` + - uninstall: allowing to remove all transitive dependencies + - able to handle the installation of dependencies with multiple providers + - multi-threaded downloads ( using aria2c ) are **not supported yet** for repository packages +- Web + - **Clean installation environment** custom action: removes all the installation environment folders ( it does not remove installed apps ) +- AppImage + - Custom actions + - **Install AppImage file**: allows to install a external AppImage file + - **Upgrade file**: allows to upgrade a manually installed AppImage file +- CLI: + - a beginning for the command line mode (`bauh-cli`). Only **list updates** command is available for now ( `bauh-cli updates` ) [#54](https://github.com/vinifmor/bauh/issues/54) +- Core + - allowing to trim the disk after all upgrades are applied ( **disabled by default**. It can be enabled on settings, Make sure your SSD supports TRIM before enabling this option. ) + - new warning dialog informing when there is a new bauh update / release available + +### Improvements +- Core + - now the root password is asked only once ( can be disabled through the new settings property `store_root_password` ) + - new upgrade model: now all packages selected to upgrade are handled at once by the underlying gem + - upgrade logs are now generated at **/tmp/bauh/logs/upgrade** +- Arch + - dependency checking algorithm + - faster for scenarios involving several packages ( taking =~ 95% less time ) + - faster for AUR installations ( taking an average of 23% less time ) + - the AUR compilation optimizations now include setting the device processors to **performance** mode + - if the pacman database is locked, a dialog is displayed requesting if the database should be unlocked to proceed with the ongoing action + - displaying missing repository dependencies sizes + - dialog design when the package cannot be uninstalled due to required packages + - removing old cached versions from the disk cache when uninstalling a package ( can be disabled on settings -> **clean_cached** ) + - database synchronization on startup ( **enabled by default**. Can be disabled on settings -> **sync_databases_startup** ) + - single pacman call to install repository dependencies + - improved conflict checking algorithm + - overall speed improvements + - code refactoring +- UI + - the name filter now delays 2 seconds before being applied + - upgrades: upgrade order and required dialogs were merged in a **single summary dialog** + - displaying the upgrade size ( Flatpak, AppImage and Arch ) + - time to determine the selected packages to upgrade takes less time + - table update performance + - tray + - treated as an application apart and not sharing the memory with the management panel ( first step to reduce its memory usage ) + - sorting types on update notification + +### Fixes +- table not displaying all updates when the "updates filter" is clicked several times +- installation logs written to the wrong temp directory +- Arch / AUR: + - sorting algorithm was providing wrong results for some cases + - not caching data about packages with no desktop entry files + +### Settings +- Default + - **pre_dependency_checking** dropped ( now is always enabled ) + - **sort_packages** dropped ( now the gems decide if it makes sense to sort the packages ) + - **disk_cache** dropped ( now is always enabled ) +- Arch / AUR: + - **transitive_checking** dropped ( now is always enabled ) + - **simple_checking** dropped ( now is always disabled ) + +### Params / Environment Variables +- param **--show-panel** dropped +- env vars **BAUH_TRAY** and **BAUH_LOGS** dropped ( the equivalent parameters remain ) +- new parameter `--settings`: opens only the settings panel +- now to open the tray use only the parameter `--tray` instead of `--tray=1` +- now to activate the logs use only the parameter `--logs` instead of `--logs=1` +- adding mutual exclusion to some parameters (`--settings`, `--tray`, `--reset`) + +### i18n contributions + - Russian (ru): [mountain-biker85](https://github.com/mountain-biker85) + + ## [0.8.5] - 2020-03-11 ### Fixes - Web diff --git a/MANIFEST.in b/MANIFEST.in index 7df4aa32..f75b1cd0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE CHANGELOG.md README.md include requirements.txt +recursive-include bauh/desktop * recursive-include bauh/view/resources * recursive-include bauh/gems/*/resources * recursive-include tests * diff --git a/README.md b/README.md index d51327fd..ca8d3bfc 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,25 @@ -**bauh** ( ba-oo ), formerly known as **fpakman**, is a graphical interface for managing your Linux applications / packages. It is able to manage AUR, AppImage, Flatpak, Snap and native Web applications. When you launch **bauh** -a management panel will be displayed where you can search, update, install, uninstall and launch applications ( downgrading is also possible in some cases ). +

+ +

+ + +**bauh** ( ba-oo ), formerly known as **fpakman**, is a graphical interface for managing your Linux applications / packages. It currently supports +the following types: AppImage, Arch ( repositories / AUR ), Flatpak, Snap and native Web applications. + +Key features: +- A management panel where you can: search, install, uninstall, launch, downgrade and retrieve the release history from software packages. +- Tray mode: launches attached to the system tray and publishes notifications when there are software updates available +- System backup: it integrates with **Timeshift** to provide a simple and safe backup process before applying any change to your system. -It has a **tray mode** ( see [Settings](https://github.com/vinifmor/bauh/tree/wgem#general-settings) ) that attaches itself as an icon to the system tray. The attached icon will get red when updates are available. This project has an official Twitter account ( **@bauh4linux** ) so people can stay on top of its news. + To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/blob/master/CONTRIBUTING.md) - -![management panel](https://raw.githubusercontent.com/vinifmor/bauh/master/pictures/panel.png) +

+ +

### Developed with @@ -24,6 +35,7 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/ - **qt5dxcb-plugin** - **python3-venv** ( only for [Manual installation](https://github.com/vinifmor/bauh/tree/wgem#manual-installation) ) - **libappindicator3** ( for the **tray mode** in GTK3 desktop environments ) +- **timeshift** ( optional: to allow system backups ) #### Arch-based distros - **python** @@ -32,10 +44,11 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/ - **python-pyqt5** - **python-yaml** - **libappindicator-gtk3** ( for the **tray mode** in GTK3 desktop environments ) +- **timeshift** ( optional: to allow system backups ) The other requirements depend on which type of applications you want to manage ( see [Gems](https://github.com/vinifmor/bauh/tree/wgem#gems--package-technology-support-) ). -### Installation +### Distribution **AUR** @@ -52,7 +65,7 @@ It may require **sudo**, but prefer the **Manual installation** described below - If you prefer a manual and isolated installation, open your favorite terminal application and type the following commands: ``` -python3 -m venv bauh_env ( creates a virtualenv in a folder called **bauh_env** ) +python3 -m venv bauh_env ( creates a virtualenv in a folder called "bauh_env" ) bauh_env/bin/pip install bauh ( installs bauh in the isolated environment ) bauh_env/bin/bauh ( launches bauh ) @@ -66,28 +79,19 @@ bauh_env/bin/pip install bauh --upgrade - To uninstall it just remove the **bauh_env** folder -- To create a shortcut ( desktop entry ) for it in your system menu ( assuming you created the isolated environment in your home folder using Python 3.7 ): - - Create a file called **bauh.desktop** in **~/.local/share/applications** with the following content -``` -[Desktop Entry] -Type=Application -Name=bauh -Comment=Install and remove applications ( AppImage, AUR, Flatpak, Snap ) -Exec=/home/$USER/bauh_env/bin/bauh -Icon=/home/$USER/bauh_env/lib/python3.7/site-packages/bauh/view/resources/img/logo.svg -``` - -- If you want a shortcut to the tray, put the **--tray=1** parameter in the end of the **Exec** line of the example above ( e.g: **Exec=/home/$USER/bauh_env/bin/bauh --tray=1** ) -- P.S: If the shortcut is not working, try to replace the **$USER** var by your user name. +- To create shortcuts ( desktop entry ) for it in your system menu ( assuming you created the isolated environment in your home folder using Python 3.7 ): + - Copy the files from **bauh/desktop** to **~/.local/share/applications** ( just replace the **Exec** field by: `Exec=/home/$USER/bauh_env/bin/bauh` ) + - Copy **logo.svg** from **bauh/view/resources/img** to **/usr/share/icons/hicolor/scalable/apps** as **bauh.svg** + - P.S: If the shortcut is not working, try to replace the **$USER** var by your user name. ### Autostart -In order to autostart the application, use your Desktop Environment settings to register it as a startup application / script (**bauh --tray=1**). Or +In order to initialize bauh when the system starts, use your Desktop Environment settings to register it as a startup application / script (**bauh-tray**). Or create a file named **bauh.desktop** in **~/.config/autostart** with the content below: ``` [Desktop Entry] Type=Application Name=bauh ( tray ) -Exec=/path/to/bauh --tray=1 +Exec=/path/to/bauh --tray ``` ### Uninstallation @@ -96,10 +100,13 @@ Before uninstalling bauh via your package manager, consider executing `bauh --re ### Gems ( package technology support ) #### Flatpak ( flatpak ) -- The user is able to search, install, uninstall, downgrade, launch and retrieve the applications history -![flatpak_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/flatpak/search.gif) +

+ +

+ +- Supported actions: search, install, uninstall, downgrade, launch, history - The configuration file is located at **~/.config/bauh/flatpak.yml** and it allows the following customizations: ``` installation_level: null # defines a default installation level: user or system. ( the popup will not be displayed if a value is defined ) @@ -109,18 +116,27 @@ installation_level: null # defines a default installation level: user or system. - Any distro: **flatpak** #### Snap ( snap ) -- The user is able to search, install, uninstall, refresh, launch and downgrade applications -![snap_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/snap/search.gif) +

+ +

+ +- Supported actions: search, install, uninstall, launch, downgrade +- Custom actions: refresh - Required dependencies: - Any distro: **snapd** ( it must be enabled after its installation. Details at https://snapcraft.io/docs/installing-snapd ) #### AppImage ( appimage ) -- The user is able to search, install, uninstall, downgrade, launch and retrieve the applications history -![appimage_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/appimage/search.gif) +

+ +

+- Supported actions: search, install, uninstall, downgrade, launch, history +- Custom actions + - **Install AppImage file**: allows to install a external AppImage file + - **Upgrade file**: allows to upgrade a manually installed AppImage file - Supported sources: [AppImageHub](https://appimage.github.io) ( **applications with no releases published to GitHub are currently not available** ) - Installed applications are store at **~/.local/share/bauh/appimage/installed** - Desktop entries ( menu shortcuts ) of the installed applications are stored at **~/.local/share/applications** @@ -141,13 +157,16 @@ db_updater: - [**fuse**](https://github.com/libfuse/libfuse) may be required to run AppImages on your system - P.S: **aria2 will only be used if multi-threaded downloads are enabled** -#### AUR ( arch ) +#### Arch ( Repositories / AUR ) + +

+ +

+ - Only available for **Arch-based systems** -- The user is able to search, install, uninstall, downgrade, launch and retrieve packages history - -![aur_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/aur/search.gif) - -- It handles conflicts, and missing / optional packages installations ( including from your distro mirrors ) +- Repository packages supported actions: search, install, uninstall, launch +- AUR packages supported actions: search, install, uninstall, downgrade, launch, history +- It handles conflicts, missing / optional packages installations, and several providers scenarios - Automatically makes simple package compilation improvements: a) if **MAKEFLAGS** is not set in **/etc/makepkg.conf**, @@ -157,16 +176,27 @@ db_updater: b) same as previous, but related to **COMPRESSXZ** and **COMPRESSZST** definitions ( if '--threads=0' is not defined ) c) **ccache** will be added to **BUILDENV** if it is installed on the system and already not defined + + d) set the device processors to performance mode Obs: For more information about them, have a look at [Makepkg](https://wiki.archlinux.org/index.php/Makepkg) - During bauh initialization a full AUR normalized index is saved at **/tmp/bauh/arch/aur.txt**, and it will only be used if the AUR API cannot handle the number of matches for a given query. -- If some of your installed packages are not categorized, send an e-mail to **bauh4linux@gmail.com** informing their names and categories in the following format: ```name=category1[,category2,category3,...]``` +- If some of your installed packages are not categorized, send an e-mail to **bauh4linux@gmail.com** informing their names and categories in the following format: ```name=category1[,category2,category3,...]``` or open a PullRequest changing [categories.txt](https://github.com/vinifmor/bauh-files/blob/master/arch/categories.txt) +- Custom actions + - **synchronize packages database**: synchronizes the database against the configured mirrors ( `sudo pacman -Syy` ) + - **refresh mirrors**: allows the user to define multiple mirrors locations and sort by the fastest ( `sudo pacman-mirrors -c country1,country2 && sudo pacman-mirrors --fasttrack 5 && sudo pacman -Syy` ) + - **quick system upgrade**: it executes a default pacman upgrade ( `pacman -Syyu --noconfirm` ) + - **clean cache**: it cleans the pacman cache diretory ( default: `/var/cache/pacman/pkg` ) - The configuration file is located at **~/.config/bauh/arch.yml** and it allows the following customizations: ``` optimize: true # if 'false': disables the auto-compilation improvements -transitive_checking: true # this property defines if dependencies of a dependency should be retrieved before the package installation. It avoids interruptions, since it will detect all required dependencies before the process begin. -sync_databases: true # package databases synchronization once a day ( or every device reboot ) before the first package installation / upgrade / downgrade -simple_checking: false # this property defines how the missing dependencies checking process should be done before installing a package. When set to 'false' an algorithm combining pacman's methods and AUR's API is used ( currently slower, but more accurate ), whereas 'false' relies only on pacman's methods ( faster. but currently not always accurate ) +sync_databases: true # package databases synchronization once a day before the first package installation / upgrade / downgrade +sync_databases_startup: true # package databases synchronization once a day during startup +clean_cached: true # defines if old cached versions should be removed from the disk cache during a package uninstallation +refresh_mirrors_startup: false # if the package mirrors should be refreshed during startup +mirrors_sort_limit: 5 # defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. +aur: true # allows to manage AUR packages +repositories: true # allows to manage packages from the configured repositories ``` - Required dependencies: - **pacman** @@ -178,16 +208,25 @@ simple_checking: false # this property defines how the missing dependencies che #### Native Web Applications ( web ) - It allows the installation of native Web applications by typing their addresses / URLs on the search bar -![url_search](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/web/url_search.gif) +

+ +

+ - It offers the possibility to customize the generated app the way you want: -![options](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/web/options.png) +

+ +

+ - It provides some suggestions coming with predefined settings, and they also can be retrieved by their names. They are defined at [suggestions.yml](https://github.com/vinifmor/bauh-files/blob/master/web/suggestions.yml), and downloaded during the application usage. -![suggestions](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/web/suggestions.gif) +

+ +

+ - It relies on [NodeJS](https://nodejs.org/en/), [Electron](https://electronjs.org/) and [nativefier](https://github.com/jiahaog/nativefier) to do all the magic, but you do not need them installed on your system. An isolated installation environment will be generated at **~/.local/share/bauh/web/env**. @@ -200,7 +239,13 @@ attach it to the generated app. - If the Tray Mode **Start Minimized** is defined during the installation setup, a desktop entry will be also generated at **~/.config/autostart** allowing the application to launch automatically after the system's boot attached to the tray. -![tray_mode](https://raw.githubusercontent.com/vinifmor/bauh/staging/pictures/web/tray.gif) +

+ +

+ + +- Specific actions + - Clean installation environment: removes all the installation environment folders ( it does not remove installed apps ) - The configuration file is located at **~/.config/bauh/web.yml** and it allows the following customizations: ``` @@ -217,19 +262,19 @@ environment: #### Environment variables / parameters You can change some application settings via environment variables or arguments (type ```bauh --help``` to get more information). -- `BAUH_TRAY (--tray )`: If the tray icon and update-check daemon should be created. Use `0` (disable, default) or `1` (enable). -- `BAUH_LOGS (--logs )`: enable **bauh** logs (for debugging purposes). Use: `0` (disable, default) or `1` (enable) -- `--reset`: cleans all configurations and cached data stored in the HOME directory +- `--tray`: if bauh should be launched attaching itself to the system tray. +- `--settings`: it displays only the settings window. +- `--reset`: it cleans all configurations and cached data stored in the HOME directory. +- `--logs`: it enables logs (for debugging purposes). #### General configuration file ( **~/.config/bauh/config.yml** ) ``` -disk_cache: - enabled: true # allows bauh to save applications icons and data to the disk to load them faster when needed download: icons: true # allows bauh to download the applications icons when they are not saved on the disk multithreaded: true # allows bauh to use a multithreaded download client installed on the system to download applications source files faster ( current only **aria2** is supported ) gems: null # defines the enabled applications types managed by bauh ( a null value means all available ) locale: null # defines a different translation for bauh ( a null value will retrieve the system's default locale ) +store_root_password: true # if the root password should be asked only once memory_cache: data_expiration: 3600 # the interval in SECONDS that data cached in memory will live icon_expiration: 300 # the interval in SECONDS that icons cached in memory will live @@ -250,25 +295,36 @@ ui: auto_scale: false # activates Qt auto screen scale factor (QT_AUTO_SCREEN_SCALE_FACTOR). It fixes scaling issues for some desktop environments ( like Gnome ) updates: check_interval: 30 # the updates checking interval in SECONDS - sort_packages: true # if the selected applications / packages to upgrade must be sorted to avoid possible issues - pre_dependency_checking: true # displays all applications / packages that must be installed before upgrading the selected ones +disk: + trim_after_update: false # it trims the hard disk after a successfull packages upgrade ( `fstrim -a -v` ) +backup: + enabled: true # generate timeshift snapshots before an action ( if timeshift is installed on the system ) + mode: 'incremental' # incremental=generates a new snapshot based on another pre-exising one. 'only_one'=deletes all pre-existing snapshots and generates a fresh one. + install: null # defines if the backup should be performed before installing a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation + uninstall: null # defines if the backup should be performed before uninstalling a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation + upgrade: null # defines if the backup should be performed before upgrading a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation + downgrade: null # defines if the backup should be performed before downgrading a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation + ``` #### Tray icons Priority: 1) Icon paths defined in **~/.config/bauh/config.yml** 2) Icons from the system with the following names: `bauh_tray_default` and `bauh_tray_updates` 3) Own packaged icons + +#### CLI +- It is a mode in which you can perform the same actions allowed in the GUI via command line. For now it only allows to check for software updates (`bauh-cli updates`). +- To verify the available commands: `bauh-cli --help`. +- To list the command parameters: `bauh-cli [command] --help`. ( e.g: `bauh-cli updates --help`) -### How to improve the performance +### How to improve performance - Disable the application types you do not want to deal with - If you don't care about restarting the app every time a new supported package technology is installed, enable `single_dependency_checking`. This can reduce the application response time, since it won't need to recheck if the required technologies are available on your system every time a given action is executed. - If you don't mind to see the applications icons, you can disable them via `download: icons: false`. The application may have a slight response improvement, since it will reduce the IO and parallelism within it. -- Let the `disk_cache` always enabled so **bauh** does not need to dynamically retrieve data every time you launch it. - ### Files and Logs - Installation logs and temporary files are saved at **/tmp/bauh** ( or **/tmp/bauh_root** if you launch it as root) -- Some data about your installed applications are stored in **~/.cache/bauh** to load them faster ( default behavior ). +- Some data about your installed applications are stored in **~/.cache/bauh** to load them faster ### [bauh-files](https://github.com/vinifmor/bauh-files) - It is a separate repository with some files downloaded during runtime. diff --git a/aur/bauh-tray b/aur/bauh-tray deleted file mode 100644 index c42e0db0..00000000 --- a/aur/bauh-tray +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -/usr/bin/bauh --tray=1 diff --git a/aur/bauh.desktop b/aur/bauh.desktop deleted file mode 100644 index c328de39..00000000 --- a/aur/bauh.desktop +++ /dev/null @@ -1,17 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Applications manager ( bauh ) -Name[pt]=Gerenciador de aplicativos ( bauh ) -Name[es]=Administrador de aplicaciones ( bauh ) -Name[ca]=Gestor d aplicacions ( bauh ) -Name[it]=Gestore delle applicazioni ( bauh ) -Name[de]=Anwendungsmanager ( bauh ) -Categories=System; -Comment=Install and remove applications ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[pt]=Instale e remova aplicativos ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[es]=Instalar y eliminar aplicaciones ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[it]=Installa e rimuovi applicazioni ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[de]=Anwendungen installieren und entfernen ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[ca]=Instal·lar i eliminar aplicacions ( AppImage, AUR, Flatpak, Snap, Web ) -Exec=/usr/bin/bauh -Icon=bauh \ No newline at end of file diff --git a/aur/bauh_tray.desktop b/aur/bauh_tray.desktop deleted file mode 100644 index 116b1ab6..00000000 --- a/aur/bauh_tray.desktop +++ /dev/null @@ -1,17 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Applications manager ( bauh - tray ) -Name[pt]=Gerenciador de aplicativos ( bauh - bandeja ) -Name[es]=Administrador de aplicaciones ( bauh - bandeja ) -Name[ca]=Gestor d aplicacions ( bauh - safata ) -Name[it]=Gestore delle applicazioni ( bauh - vassoio ) -Name[de]=Anwendungsmanager ( bauh - tablett ) -Categories=System; -Comment=Install and remove applications ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[pt]=Instale e remova aplicativos ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[es]=Instalar y eliminar aplicaciones ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[it]=Installa e rimuovi applicazioni ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[de]=Anwendungen installieren und entfernen ( AppImage, AUR, Flatpak, Snap, Web ) -Comment[ca]=Instal·lar i eliminar aplicacions ( AppImage, AUR, Flatpak, Snap, Web ) -Exec=/usr/bin/bauh --tray=1 -Icon=bauh \ No newline at end of file diff --git a/aur/copy_icons.py b/aur/copy_icons.py deleted file mode 100644 index 6c684f71..00000000 --- a/aur/copy_icons.py +++ /dev/null @@ -1,14 +0,0 @@ -import glob -import shutil -import sys -from pathlib import Path - -output_base = sys.argv[2] - -for f in glob.glob(sys.argv[1] + '/*.png'): - res = f.split('/')[-1].split('.')[0] - dest_dir = output_base + '/' + res + '/apps' - - Path(dest_dir).mkdir(parents=True, exist_ok=True) - - shutil.copy(f, dest_dir + '/bauh.png') diff --git a/aur/icons/128x128.png b/aur/icons/128x128.png deleted file mode 100644 index 53088ba37d8e7612d7f667f0140e24eaa2f9fbd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6580 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 P00000NkvXXu0mjfn@pN@ diff --git a/aur/icons/16x16.png b/aur/icons/16x16.png deleted file mode 100644 index b8b476ca5c7c9b0848ab594ac30b8524e5f47e24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1011 zcmVEX>4Tx04R}tkv&MmKpe$iQ>9WW7R(^xkfAzRC@SKpRVYG*P%E_RU_SZkM+H^bh|{W*Vj)BONgw~P>leu-ldA%S z91EyHgXH?b{@{1FR&jE|ONu0c-WSLD7zIMRK&#<6-^Y&AIst;uz?I(d*P6i0C+Urj z7CQn4wtQ&$$tyU3izn^q&_yKjPQJJ!gb;B>C*~H^a!*!t zAI=}QaY)-Al~PVCVN0Boh@HyQV&iW_Bhr2NeuwJV2Zh4EvT#1%qrQnEgd8SFZA$a=W6sGt0 z;m0ZgpOc{9iQR128)93~X1!G9?eZo|xgDg&$ZBJo=4O@C-ouOz4|Iy1U-(tfrdX_c9swMu6#TL1=*6 hFEZKW!})wq_9r`2v%dj`XYv35002ovPDHLkV1h^6(uV*5 diff --git a/aur/icons/24x24.png b/aur/icons/24x24.png deleted file mode 100644 index 2f4eb52559dd50065a526546dab8dfea787d04b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1433 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 diff --git a/aur/icons/32x32.png b/aur/icons/32x32.png deleted file mode 100644 index c84ecae905a67cc278b168ac4a2b745615d7c350..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1667 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 diff --git a/aur/icons/512x512.png b/aur/icons/512x512.png deleted file mode 100644 index 38b946cdb761a4dae8ac605e04e5fb301855d749..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22325 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 diff --git a/aur/icons/8x8.png b/aur/icons/8x8.png deleted file mode 100644 index e73905c56d6a881ddbf2ddc6a85a1d277621b3be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 726 zcmV;{0xA88P)EX>4Tx04R}tkv&MmKpe$iQ>9WW7R(^xkfAzRC@SKpRVYG*P%E_RU_SZkM+H^bh|{W*Vj)BONgw~P>leu-ldA%S z91EyHgXH?b{@{1FR&jE|ONu0c-WSLD7zIMRK&#<6-^Y&AIst;uz?I(d*P6i0C+Urj z7CQn4wt~4_{lmY1|2_En=RX5upthvLe+GsI78?nBAukPvTYKdgMA^46+*$bG z5Ca2419M-z{{7$o{wh3q%E`bYp~mp}7eB*?E7usb*xBWMO;q9;&on#zXJBApc=hoI z!-<>k86Lm=#bBZ!&LGUo$-uzCz`())1!DXh41Q*k40oO|G9Uvc1_FSIfq}u1fq~%w z7P*5A3=B?;3=9kmCUV>iGg8eR7#J8DpjI??Y(BC7!n3ao0P6ES9k41`KL7v#07*qo IM6N<$f_daPfdBvi diff --git a/aur/icons/96x96.png b/aur/icons/96x96.png deleted file mode 100644 index 9de5638d54bb973ba4560e9f5337de018af38472..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4429 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 List[SoftwarePackage]: + def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements: """ - sorts the best order to perform the update of some packages - :param pkgs: - :return: - """ - return pkgs - - def get_update_requirements(self, pkgs: List[SoftwarePackage], watcher: ProcessWatcher) -> List[SoftwarePackage]: - """ - return additional required software that needs to be installed before updating a list of packages + return additional required software that needs to be installed / removed / updated before updating a list of packages :param pkgs: :param watcher :return: """ - return [] + return UpgradeRequirements(None, None, [UpgradeRequirement(p) for p in pkgs], None) @abstractmethod - def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: """ - :param pkg: + :param requirements: :param root_password: the root user password (if required) :param watcher: :return: @@ -182,7 +207,7 @@ class SoftwareManager(ABC): :param only_icon: if only the icon should be saved :return: """ - if self.context.disk_cache and pkg.supports_disk_cache(): + if pkg.supports_disk_cache(): self.serialize_to_disk(pkg, icon_bytes, only_icon) def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool): @@ -217,7 +242,7 @@ class SoftwareManager(ABC): @abstractmethod def requires_root(self, action: str, pkg: SoftwarePackage): """ - if a given action requires root privileges to be executed. Current actions are: 'install', 'uninstall', 'downgrade', 'search', 'refresh' + if a given action requires root privileges to be executed. Current actions are: 'install', 'uninstall', 'downgrade', 'search', 'refresh', 'prepare' :param action: :param pkg: :return: @@ -225,9 +250,11 @@ class SoftwareManager(ABC): pass @abstractmethod - def prepare(self): + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): """ It prepares the manager to start working. It will be called by GUI. Do not call it within. + :param task_manager: a task manager instance used to register ongoing tasks during prepare + :param root_password :return: """ pass @@ -257,7 +284,7 @@ class SoftwareManager(ABC): """ pass - def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: """ At the moment the GUI implements this action. No need to implement it yourself. :param action: @@ -304,3 +331,12 @@ class SoftwareManager(ABC): :return: a tuple with a bool informing if the settings were saved and a list of error messages """ pass + + def get_custom_actions(self) -> List[CustomSoftwareAction]: + """ + :return: custom actions + """ + pass + + def fill_sizes(self, pkgs: List[SoftwarePackage]): + pass diff --git a/bauh/api/abstract/handler.py b/bauh/api/abstract/handler.py index 3f8ec64e..cfbf4a4e 100644 --- a/bauh/api/abstract/handler.py +++ b/bauh/api/abstract/handler.py @@ -17,7 +17,7 @@ class ProcessWatcher: """ pass - def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None)-> bool: + def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None, deny_button: bool = True)-> bool: """ request a user confirmation. In the current GUI implementation, it shows a popup to the user. :param title: popup title @@ -25,10 +25,17 @@ class ProcessWatcher: :param components: extra view components that will be rendered to the confirmation popup. :param confirmation_label: optional confirmation button label (default to 'yes') :param deny_label: optional deny button label (default to 'no') + :param deny_button: if the deny button should be displayed :return: if the request was confirmed by the user """ pass + def request_reboot(self, msg: str) -> bool: + """ + :return: requests a system reboot + """ + pass + def show_message(self, title: str, body: str, type_: MessageType = MessageType.INFO): """ shows a message to the user. In the current GUI implementation, it shows a popup. @@ -70,4 +77,33 @@ class ProcessWatcher: """ asks the root password for the user :return: a tuple with the typed password and if it is valid - """ \ No newline at end of file + """ + + +class TaskManager: + + def register_task(self, id_: str, label: str, icon_path: str): + """ + :param id_: an unique identifier for the task + :param label: an i18n label + :param icon_path: str + :return: + """ + pass + + def update_progress(self, task_id: str, progress: float, substatus: str): + """ + :param task_id: + :param progress: a float between 0 and 100. + :param substatus: a substatus string representing the current state + :return: + """ + pass + + def finish_task(self, task_id: str): + """ + marks a task as finished + :param task_id: + :return: + """ + pass diff --git a/bauh/api/abstract/model.py b/bauh/api/abstract/model.py index 1500b3f9..f8f75913 100644 --- a/bauh/api/abstract/model.py +++ b/bauh/api/abstract/model.py @@ -5,22 +5,31 @@ from typing import List from bauh.api.constants import CACHE_PATH -class PackageAction: +class CustomSoftwareAction: - def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool): + def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool, manager: "SoftwareManager" = None, backup: bool = False): """ :param i18_label_key: the i18n key that will be used to display the action name :param i18n_status_key: the i18n key that will be used to display the action name being executed :param icon_path: the action icon path. Use None for no icon :param manager_method: the SoftwareManager method name that should be called. The method must has the following parameters: (pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) + :param manager: the instance that will execute the action ( optional ) + :param backup: if a system backup should be performed before executing the action :param requires_root: """ - self.id = id self.i18_label_key = i18_label_key self.i18n_status_key = i18n_status_key self.icon_path = icon_path self.manager_method = manager_method self.requires_root = requires_root + self.manager = manager + self.backup = backup + + def __hash__(self): + return self.i18_label_key.__hash__() + self.i18n_status_key.__hash__() + self.manager_method.__hash__() + + def __repr__(self): + return "CustomAction (label={}, method={})".format(self.i18_label_key, self.manager_method) class PackageStatus(Enum): @@ -58,6 +67,7 @@ class SoftwarePackage(ABC): self.size = size self.categories = categories self.license = license + self.gem_name = self.__module__.split('.')[2] @abstractmethod def has_history(self): @@ -122,11 +132,21 @@ class SoftwarePackage(ABC): """ return CACHE_PATH + '/' + self.get_type() + def can_be_updated(self) -> bool: + """ + :return: if the package can be updated. + """ + return self.installed and self.update + def get_disk_icon_path(self): - return '{}/icon.png'.format(self.get_disk_cache_path()) + path = self.get_disk_cache_path() + if path: + return '{}/icon.png'.format(path) def get_disk_data_path(self): - return '{}/data.json'.format(self.get_disk_cache_path()) + path = self.get_disk_cache_path() + if path: + return '{}/data.json'.format(path) @abstractmethod def get_data_to_cache(self) -> dict: @@ -163,7 +183,7 @@ class SoftwarePackage(ABC): """ pass - def get_custom_supported_actions(self) -> List[PackageAction]: + def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: """ :return: custom supported actions """ @@ -181,24 +201,35 @@ class SoftwarePackage(ABC): """ return self.name + def get_display_name(self) -> str: + """ + :return: name displayed on the table + """ + return self.name + + @abstractmethod + def supports_backup(self) -> bool: + pass + def __str__(self): return '{} (id={}, name={})'.format(self.__class__.__name__, self.id, self.name) class PackageUpdate: - def __init__(self, pkg_id: str, version: str, pkg_type: str): + def __init__(self, pkg_id: str, version: str, pkg_type: str, name: str): """ :param pkg_id: an unique package identifier :param version: the new version :param pkg_type: the package type """ self.id = pkg_id + self.name = name self.version = version self.type = pkg_type def __str__(self): - return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type) + return '{} (id={}, name={}, version={}, type={})'.format(self.__class__.__name__, self.id, self.name, self.version, self.type) class PackageHistory: diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 865563fa..2cad07f1 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -75,7 +75,8 @@ class SelectViewType(Enum): class SingleSelectComponent(InputViewComponent): def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, - max_per_line: int = 1, tooltip: str = None, max_width: int = -1, id_: str = None): + max_per_line: int = 1, tooltip: str = None, max_width: int = -1, id_: str = None, + capitalize_label: bool = True): super(SingleSelectComponent, self).__init__(id_=id_) self.type = type_ self.label = label @@ -84,6 +85,7 @@ class SingleSelectComponent(InputViewComponent): self.max_per_line = max_per_line self.tooltip = tooltip self.max_width = max_width + self.capitalize_label = capitalize_label def get_selected(self): if self.value: @@ -119,11 +121,12 @@ class MultipleSelectComponent(InputViewComponent): class TextComponent(ViewComponent): - def __init__(self, html: str, max_width: int = -1, tooltip: str = None, id_: str = None): + def __init__(self, html: str, max_width: int = -1, tooltip: str = None, id_: str = None, size: int = None): super(TextComponent, self).__init__(id_=id_) self.value = html self.max_width = max_width self.tooltip = tooltip + self.size = size class TwoStateButtonComponent(ViewComponent): @@ -156,8 +159,10 @@ class TextInputComponent(ViewComponent): def get_int_value(self) -> int: if self.value is not None: - return int(self.value) - return None + val = self.value.strip() if isinstance(self.value, str) else self.value + + if val: + return int(self.value) class FormComponent(ViewComponent): diff --git a/bauh/api/constants.py b/bauh/api/constants.py index 0a24360e..3aff0922 100644 --- a/bauh/api/constants.py +++ b/bauh/api/constants.py @@ -1,7 +1,7 @@ import os from pathlib import Path -CACHE_PATH = '{}/.cache/bauh'.format(Path.home()) -CONFIG_PATH = '{}/.config/bauh'.format(Path.home()) -DESKTOP_ENTRIES_DIR = '{}/.local/share/applications'.format(Path.home()) +CACHE_PATH = '{}/.cache/bauh'.format(str(Path.home())) +CONFIG_PATH = '{}/.config/bauh'.format(str(Path.home())) +DESKTOP_ENTRIES_DIR = '{}/.local/share/applications'.format(str(Path.home())) TEMP_DIR = '/tmp/bauh{}'.format('_root' if os.getuid() == 0 else '') diff --git a/bauh/api/http.py b/bauh/api/http.py index 2a1c91be..6da9b161 100644 --- a/bauh/api/http.py +++ b/bauh/api/http.py @@ -67,7 +67,7 @@ class HttpClient: res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session) return yaml.safe_load(res.text) if res else None - def get_content_length(self, url: str, session: bool = True) -> str: + def get_content_length_in_bytes(self, url: str, session: bool = True) -> int: params = {'url': url, 'allow_redirects': True, 'stream': True} if session: res = self.session.get(**params) @@ -77,8 +77,17 @@ class HttpClient: if res.status_code == 200: size = res.headers.get('Content-Length') - if size is not None: - return system.get_human_size_str(size) + if size: + try: + return int(size) + except: + pass + + def get_content_length(self, url: str, session: bool = True) -> str: + size = self.get_content_length_in_bytes(url, session) + + if size: + return system.get_human_size_str(size) def exists(self, url: str, session: bool = True) -> bool: params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': 5} diff --git a/bauh/app.py b/bauh/app.py index 85eee62b..bcfed409 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -1,28 +1,15 @@ import os import sys -from threading import Thread import urllib3 -from PyQt5.QtCore import Qt, QCoreApplication -from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QCoreApplication, Qt -from bauh import __version__, __app_name__, app_args, ROOT_DIR -from bauh.api.abstract.controller import ApplicationContext -from bauh.api.http import HttpClient -from bauh.view.core import gems, config -from bauh.view.core.controller import GenericSoftwareManager -from bauh.view.core.downloader import AdaptableFileDownloader -from bauh.view.qt.systray import TrayIcon -from bauh.view.qt.window import ManageWindow -from bauh.view.util import util, logs, translation -from bauh.view.util.cache import DefaultMemoryCacheFactory, CacheCleaner -from bauh.view.util.disk import DefaultDiskCacheLoaderFactory -from bauh.view.util.translation import I18n - -DEFAULT_I18N_KEY = 'en' +from bauh import __app_name__, app_args +from bauh.view.core import config +from bauh.view.util import logs -def main(): +def main(tray: bool = False): if not os.getenv('PYTHONUNBUFFERED'): os.environ['PYTHONUNBUFFERED'] = '1' @@ -32,91 +19,31 @@ def main(): logger = logs.new_logger(__app_name__, bool(args.logs)) - local_config = config.read_config(update_file=True) + app_config = config.read_config(update_file=True) - if local_config['ui']['auto_scale']: + if bool(app_config['ui']['auto_scale']): os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1' logger.info("Auto screen scale factor activated") - if local_config['ui']['hdpi']: + if bool(app_config['ui']['hdpi']): logger.info("HDPI settings activated") QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) - i18n_key, current_i18n = translation.get_locale_keys(local_config['locale']) - default_i18n = translation.get_locale_keys(DEFAULT_I18N_KEY)[1] if i18n_key != DEFAULT_I18N_KEY else {} - i18n = I18n(i18n_key, current_i18n, DEFAULT_I18N_KEY, default_i18n) - - cache_cleaner = CacheCleaner() - cache_factory = DefaultMemoryCacheFactory(expiration_time=int(local_config['memory_cache']['data_expiration']), cleaner=cache_cleaner) - icon_cache = cache_factory.new(int(local_config['memory_cache']['icon_expiration'])) - - http_client = HttpClient(logger) - - context = ApplicationContext(i18n=i18n, - http_client=http_client, - disk_cache=bool(local_config['disk_cache']['enabled']), - download_icons=bool(local_config['download']['icons']), - app_root_dir=ROOT_DIR, - cache_factory=cache_factory, - disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=bool(local_config['disk_cache']['enabled']), logger=logger), - logger=logger, - distro=util.get_distro(), - file_downloader=AdaptableFileDownloader(logger, bool(local_config['download']['multithreaded']), - i18n, http_client), - app_name=__app_name__) - - managers = gems.load_managers(context=context, locale=i18n_key, config=local_config, default_locale=DEFAULT_I18N_KEY) - - if args.reset: - util.clean_app_files(managers) - exit(0) - - manager = GenericSoftwareManager(managers, context=context, config=local_config) - manager.prepare() - - app = QApplication(sys.argv) - app.setQuitOnLastWindowClosed(False) # otherwise windows opened through the tray icon kill the aplication when closed - app.setApplicationName(__app_name__) - app.setApplicationVersion(__version__) - app_icon = util.get_default_icon()[1] - app.setWindowIcon(app_icon) - - if local_config['ui']['style']: - app.setStyle(str(local_config['ui']['style'])) + if tray or bool(args.tray): + from bauh.tray import new_tray_icon + app, widget = new_tray_icon(app_config) else: - if app.style().objectName().lower() not in {'fusion', 'breeze'}: - app.setStyle('Fusion') + from bauh.manage import new_manage_panel + app, widget = new_manage_panel(args, app_config, logger) - manage_window = ManageWindow(i18n=i18n, - manager=manager, - icon_cache=icon_cache, - screen_size=app.primaryScreen().size(), - config=local_config, - context=context, - http_client=http_client, - icon=app_icon, - logger=logger) - - if args.tray: - tray_icon = TrayIcon(i18n=i18n, - manager=manager, - manage_window=manage_window, - screen_size=app.primaryScreen().size(), - config=local_config) - manage_window.set_tray_icon(tray_icon) - tray_icon.show() - - if args.show_panel: - tray_icon.show_manage_window() - else: - manage_window.refresh_apps() - manage_window.show() - - cache_cleaner.start() - Thread(target=config.remove_old_config, args=(logger,), daemon=True).start() + widget.show() sys.exit(app.exec_()) +def tray(): + main(tray=True) + + if __name__ == '__main__': main() diff --git a/bauh/app_args.py b/bauh/app_args.py index 52fbdbfe..ca35e626 100644 --- a/bauh/app_args.py +++ b/bauh/app_args.py @@ -1,17 +1,19 @@ import argparse -import logging -import os from argparse import Namespace from bauh import __app_name__, __version__ def read() -> Namespace: - parser = argparse.ArgumentParser(prog=__app_name__, description="GUI for Linux package management") + parser = argparse.ArgumentParser(prog=__app_name__, description="GUI for Linux software management") parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) - parser.add_argument('--tray', action="store", default=os.getenv('BAUH_TRAY', 0), choices=[0, 1], type=int, - help='If the tray icon and update-check daemon should be created. Default: %(default)s') - parser.add_argument('--logs', action="store", default=int(os.getenv('BAUH_LOGS', 0)), choices=[0, 1], type=int, help='If the application logs should be displayed. Default: %(default)s') - parser.add_argument('--show-panel', action="store_true", help='Shows the management panel after the app icon is attached to the tray.') - parser.add_argument('--reset', action="store_true", help='Removes all configuration and cache files') + parser.add_argument('--logs', action="store_true", help='It activates {} logs.'.format(__app_name__)) + + exclusive_args = parser.add_mutually_exclusive_group() + exclusive_args.add_argument('--tray', action="store_true", help='If {} should be attached to the system tray.'.format(__app_name__)) + exclusive_args.add_argument('--settings', action="store_true", help="Display only the settings panel") + exclusive_args.add_argument('--reset', action="store_true", help='Remove all configuration and cache files') + + parser.add_argument_group(exclusive_args) + return parser.parse_args() diff --git a/bauh/cli/__init__.py b/bauh/cli/__init__.py new file mode 100644 index 00000000..bd765fef --- /dev/null +++ b/bauh/cli/__init__.py @@ -0,0 +1,3 @@ +from bauh import __app_name__ + +__app_name__ = '{}-cli'.format(__app_name__) diff --git a/bauh/cli/app.py b/bauh/cli/app.py new file mode 100644 index 00000000..76ff514a --- /dev/null +++ b/bauh/cli/app.py @@ -0,0 +1,56 @@ +import os + +import urllib3 + +from bauh import ROOT_DIR +from bauh.api.abstract.context import ApplicationContext +from bauh.api.http import HttpClient +from bauh.cli import __app_name__, cli_args +from bauh.cli.controller import CLIManager +from bauh.view.core import config, gems +from bauh.view.core.controller import GenericSoftwareManager +from bauh.view.core.downloader import AdaptableFileDownloader +from bauh.view.util import logs, util +from bauh.view.util.cache import DefaultMemoryCacheFactory +from bauh.view.util.disk import DefaultDiskCacheLoaderFactory +from bauh.view.util.translation import I18n + + +def main(): + if not os.getenv('PYTHONUNBUFFERED'): + os.environ['PYTHONUNBUFFERED'] = '1' + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + args = cli_args.read() + logger = logs.new_logger(__app_name__, False) + + app_config = config.read_config(update_file=True) + http_client = HttpClient(logger) + + empty_i18n = I18n('en', {}, 'en', {}) + + cache_factory = DefaultMemoryCacheFactory(expiration_time=0) + + context = ApplicationContext(i18n=empty_i18n, + http_client=http_client, + download_icons=bool(app_config['download']['icons']), + app_root_dir=ROOT_DIR, + cache_factory=cache_factory, + disk_loader_factory=DefaultDiskCacheLoaderFactory(logger), + logger=logger, + distro=util.get_distro(), + file_downloader=AdaptableFileDownloader(logger, bool(app_config['download']['multithreaded']), + empty_i18n, http_client), + app_name=__app_name__) + + managers = gems.load_managers(context=context, locale=empty_i18n.current_key, config=app_config, default_locale='en') + + cli = CLIManager(GenericSoftwareManager(managers, context=context, config=app_config)) + + if args.command == 'updates': + cli.list_updates(args.format) + + +if __name__ == '__main__': + main() diff --git a/bauh/cli/cli_args.py b/bauh/cli/cli_args.py new file mode 100644 index 00000000..ff36e6b1 --- /dev/null +++ b/bauh/cli/cli_args.py @@ -0,0 +1,15 @@ +import argparse +from argparse import Namespace + +from bauh import __app_name__, __version__ + + +def read() -> Namespace: + parser = argparse.ArgumentParser(prog='{}-cli'.format(__app_name__), description="CLI for Linux software management") + parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) + + sub_parsers = parser.add_subparsers(dest='command', help='commands') + updates_parser = sub_parsers.add_parser('updates', help='List available software updates') + updates_parser.add_argument('-f', '--format', help='Command output format. Default: %(default)s', choices=['text', 'json'], default='text') + + return parser.parse_args() diff --git a/bauh/cli/controller.py b/bauh/cli/controller.py new file mode 100644 index 00000000..35dad789 --- /dev/null +++ b/bauh/cli/controller.py @@ -0,0 +1,30 @@ +import json + +from bauh.cli import __app_name__ +from bauh.view.core.controller import GenericSoftwareManager + + +class CLIManager: + + def __init__(self, manager: GenericSoftwareManager): + self.manager = manager + + def _print(self, msg: str): + print('[{}] {}'.format(__app_name__, msg)) + + def list_updates(self, output_format: str): + updates = self.manager.list_updates() + + json_output = output_format == 'json' + + if not updates and not json_output: + self._print('No updates available') + return + + if not json_output: + self._print('There are {} updates available:\n'.format(len(updates))) + + for idx, u in enumerate(updates): + print('{}. Name: {}\tVersion: {}\tType: {}'.format(idx+1, u.name, u.version, u.type)) + else: + print(json.dumps([u.__dict__ for u in updates])) diff --git a/bauh/commons/category.py b/bauh/commons/category.py index bbf4bc29..aa46bd0c 100644 --- a/bauh/commons/category.py +++ b/bauh/commons/category.py @@ -14,22 +14,23 @@ from bauh.api.http import HttpClient class CategoriesDownloader(Thread): def __init__(self, id_: str, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager, - disk_cache: bool, url_categories_file: str, disk_cache_dir: str, categories_path: str): + url_categories_file: str, disk_cache_dir: str, categories_path: str, before=None, after=None): super(CategoriesDownloader, self).__init__(daemon=True) self.id_ = id_ self.http_client = http_client self.logger = logger self.manager = manager - self.disk_cache = disk_cache self.url_categories_file = url_categories_file self.disk_cache_dir = disk_cache_dir self.categories_path = categories_path + self.before = before + self.after = after def _msg(self, msg: str): return '{}({}): {}'.format(self.__class__.__name__, self.id_, msg) def _read_categories_from_disk(self) -> Dict[str, List[str]]: - if self.disk_cache and os.path.exists(self.categories_path): + if os.path.exists(self.categories_path): self.logger.info(self._msg("Reading cached categories from the disk")) with open(self.categories_path) as f: @@ -73,7 +74,7 @@ class CategoriesDownloader(Thread): categories = self._map_categories(res.text) self.logger.info(self._msg('Loaded categories for {} applications'.format(len(categories)))) - if self.disk_cache and categories: + if categories: Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start() return categories @@ -97,6 +98,9 @@ class CategoriesDownloader(Thread): self._set_categories(self.download_categories()) def run(self): + if self.before: + self.before() + cached = self._read_categories_from_disk() if cached: @@ -105,4 +109,7 @@ class CategoriesDownloader(Thread): else: self._download_and_set() + if self.after: + self.after() + self.logger.info(self._msg('Finished')) diff --git a/bauh/commons/html.py b/bauh/commons/html.py index 7ca8bbbe..1d269b09 100644 --- a/bauh/commons/html.py +++ b/bauh/commons/html.py @@ -9,3 +9,7 @@ def strip_html(string: str): def bold(text: str) -> str: return '{}'.format(text) + + +def link(url: str) -> str: + return '{}'.format(url, url) diff --git a/bauh/commons/internet.py b/bauh/commons/internet.py index eb52556f..9cd86324 100644 --- a/bauh/commons/internet.py +++ b/bauh/commons/internet.py @@ -1,16 +1,13 @@ -import logging - -import requests - -from bauh.api.http import HttpClient +import subprocess +import traceback +from subprocess import Popen -def is_available(client: HttpClient, logger: logging.Logger) -> bool: +def is_available() -> bool: try: - client.exists('https://google.com') - return True - except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): - if logger: - logger.warning('Internet connection seems to be off') + res = Popen(['ping', '-q', '-w1', '-c1', 'google.com'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + res.wait() + return res.returncode == 0 + except: + traceback.print_exc() return False - diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 23be8442..e75ce501 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -106,7 +106,7 @@ class ProcessHandler: if self.watcher: self.watcher.print(msg) - def handle(self, process: SystemProcess, error_output: StringIO = None) -> bool: + def handle(self, process: SystemProcess, error_output: StringIO = None, output_handler=None) -> bool: self._notify_watcher(' '.join(process.subproc.args) + '\n') already_succeeded = False @@ -117,6 +117,9 @@ class ProcessHandler: if line: self._notify_watcher(line) + if output_handler: + output_handler(line) + if process.success_phrases and [p in line for p in process.success_phrases]: already_succeeded = True @@ -132,6 +135,9 @@ class ProcessHandler: if line: self._notify_watcher(line) + if output_handler: + output_handler(line) + if error_output is not None: error_output.write(line) @@ -241,6 +247,10 @@ def get_dir_size(start_path='.'): def get_human_size_str(size) -> str: int_size = int(size) + + if int_size == 0: + return '0' + for m in SIZE_MULTIPLIERS: size_str = str(int_size * m[0]) diff --git a/bauh/commons/util.py b/bauh/commons/util.py index 31f644b0..4b873f7e 100644 --- a/bauh/commons/util.py +++ b/bauh/commons/util.py @@ -9,3 +9,20 @@ def deep_update(source: dict, overrides: dict): else: source[key] = overrides[key] return source + + +def size_to_byte(size: float, unit: str) -> int: + lower_unit = unit.lower() + + if lower_unit[0] == 'b': + final_size = size + elif lower_unit[0] == 'k': + final_size = size * 1000 + elif lower_unit[0] == 'm': + final_size = size * 1000000 + elif lower_unit[0] == 't': + final_size = size * 1000000000000 + else: + final_size = size * 1000000000000000 + + return int(final_size) diff --git a/bauh/context.py b/bauh/context.py new file mode 100644 index 00000000..63586ae8 --- /dev/null +++ b/bauh/context.py @@ -0,0 +1,46 @@ +import sys +from typing import Tuple + +from PyQt5.QtWidgets import QApplication + +from bauh import __app_name__, __version__ +from bauh.view.util import util, translation +from bauh.view.util.translation import I18n + +DEFAULT_I18N_KEY = 'en' + + +def new_qt_application(app_config: dict, quit_on_last_closed: bool = False, name: str = None) -> QApplication: + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(quit_on_last_closed) # otherwise windows opened through the tray icon kill the application when closed + app.setApplicationName(name if name else __app_name__) + app.setApplicationVersion(__version__) + app.setWindowIcon(util.get_default_icon()[1]) + + if app_config['ui']['style']: + app.setStyle(str(app_config['ui']['style'])) + else: + if app.style().objectName().lower() not in {'fusion', 'breeze'}: + app.setStyle('Fusion') + + return app + + +def _gen_i18n_data(app_config: dict, locale_dir: str) -> Tuple[str, dict, str, dict]: + i18n_key, current_i18n = translation.get_locale_keys(app_config['locale'], locale_dir=locale_dir) + default_i18n = translation.get_locale_keys(DEFAULT_I18N_KEY, locale_dir=locale_dir)[1] if i18n_key != DEFAULT_I18N_KEY else {} + return i18n_key, current_i18n, DEFAULT_I18N_KEY, default_i18n + + +def generate_i18n(app_config: dict, locale_dir: str) -> I18n: + return I18n(*_gen_i18n_data(app_config, locale_dir)) + + +def update_i18n(app_config, locale_dir: str, i18n: I18n) -> I18n: + cur_key, cur_dict, def_key, def_dict = _gen_i18n_data(app_config, locale_dir) + + if i18n.current_key == cur_key: + i18n.current.update(cur_dict) + + i18n.default.update(def_dict) + return i18n diff --git a/bauh/desktop/bauh.desktop b/bauh/desktop/bauh.desktop new file mode 100644 index 00000000..24c5e28c --- /dev/null +++ b/bauh/desktop/bauh.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +Type=Application +Name=Applications (bauh) +Name[pt]=Aplicativos (bauh) +Name[es]=Aplicaciones (bauh) +Name[ca]=Aplicacions (bauh) +Name[it]=Applicazioni (bauh) +Name[de]=Anwendungen (bauh) +Categories=System; +Comment=Install and remove applications ( AppImage, Arch, Flatpak, Snap, Web ) +Comment[pt]=Instale e remova aplicativos ( AppImage, Arch, Flatpak, Snap, Web ) +Comment[es]=Instalar y eliminar aplicaciones ( AppImage, Arch, Flatpak, Snap, Web ) +Comment[it]=Installa e rimuovi applicazioni ( AppImage, Arch, Flatpak, Snap, Web ) +Comment[de]=Anwendungen installieren und entfernen ( AppImage, Arch, Flatpak, Snap, Web ) +Comment[ca]=Instal·lar i eliminar aplicacions ( AppImage, Arch, Flatpak, Snap, Web ) +Exec=/usr/bin/bauh +Icon=bauh \ No newline at end of file diff --git a/bauh/desktop/bauh_tray.desktop b/bauh/desktop/bauh_tray.desktop new file mode 100644 index 00000000..eccfe763 --- /dev/null +++ b/bauh/desktop/bauh_tray.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +Type=Application +Name=Applications (bauh-tray) +Name[pt]=Aplicativos (bauh-bandeja) +Name[es]=Aplicaciones (bauh-bandeja) +Name[ca]=Aplicacions (bauh-safata) +Name[it]=Applicazioni (bauh-vassoio) +Name[de]=Anwendungen (bauh - tablett) +Categories=System; +Comment=Install and remove applications (AppImage, Arch, Flatpak, Snap, Web) +Comment[pt]=Instale e remova aplicativos (AppImage, Arch, Flatpak, Snap, Web) +Comment[es]=Instalar y eliminar aplicaciones (AppImage, Arch, Flatpak, Snap, Web) +Comment[it]=Installa e rimuovi applicazioni (AppImage, Arch, Flatpak, Snap, Web) +Comment[de]=Anwendungen installieren und entfernen (AppImage, Arch, Flatpak, Snap, Web) +Comment[ca]=Instal·lar i eliminar aplicacions (AppImage, Arch, Flatpak, Snap, Web) +Exec=/usr/bin/bauh-tray +Icon=bauh \ No newline at end of file diff --git a/bauh/gems/appimage/__init__.py b/bauh/gems/appimage/__init__.py index 7bb34b63..74073e52 100644 --- a/bauh/gems/appimage/__init__.py +++ b/bauh/gems/appimage/__init__.py @@ -2,9 +2,14 @@ import os from pathlib import Path from bauh.api.constants import CONFIG_PATH +from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -LOCAL_PATH = '{}/.local/share/bauh/appimage'.format(Path.home()) +LOCAL_PATH = '{}/.local/share/bauh/appimage'.format(str(Path.home())) INSTALLATION_PATH = LOCAL_PATH + '/installed/' SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/suggestions.txt' CONFIG_FILE = '{}/appimage.yml'.format(CONFIG_PATH) + + +def get_icon_path() -> str: + return resource.get_path('img/appimage.svg', ROOT_DIR) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index b1129541..99f5a6f0 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -9,23 +9,24 @@ import traceback from datetime import datetime from math import floor from pathlib import Path -from threading import Lock, Thread +from threading import Lock from typing import Set, Type, List, Tuple from colorama import Fore from bauh.api.abstract.context import ApplicationContext -from bauh.api.abstract.controller import SoftwareManager, SearchResult +from bauh.api.abstract.controller import SoftwareManager, SearchResult, UpgradeRequirements, UpgradeRequirement from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ - SuggestionPriority + SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, ViewComponent, FormComponent, InputOption, SingleSelectComponent, \ - SelectViewType, TextInputComponent, PanelComponent + SelectViewType, TextInputComponent, PanelComponent, FileChooserComponent +from bauh.commons import resource from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess -from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE, CONFIG_FILE +from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE, CONFIG_FILE, ROOT_DIR from bauh.gems.appimage.config import read_config from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.worker import DatabaseUpdater @@ -52,6 +53,86 @@ class AppImageManager(SoftwareManager): self.logger = context.logger self.file_downloader = context.file_downloader self.db_locks = {DB_APPS_PATH: Lock(), DB_RELEASES_PATH: Lock()} + self.custom_actions = [CustomSoftwareAction(i18_label_key='appimage.custom_action.install_file', + i18n_status_key='appimage.custom_action.install_file.status', + manager=self, + manager_method='install_file', + icon_path=resource.get_path('img/appimage.svg', ROOT_DIR), + requires_root=False)] + + def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: + file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}) + input_name = TextInputComponent(label=self.i18n['name'].capitalize()) + input_version = TextInputComponent(label=self.i18n['version'].capitalize()) + input_description = TextInputComponent(label=self.i18n['description'].capitalize()) + + cat_ops = [InputOption(label=self.i18n['category.none'].capitalize(), value=0)] + cat_ops.extend([InputOption(label=self.i18n[c.lower()].capitalize(), value=c) for c in self.context.default_categories]) + inp_cat = SingleSelectComponent(label=self.i18n['category'], type_=SelectViewType.COMBO, options=cat_ops, + default_option=cat_ops[0]) + + form = FormComponent(label='', components=[file_chooser, input_name, input_version, input_description, inp_cat], spaces=False) + + while True: + if watcher.request_confirmation(title=self.i18n['appimage.custom_action.install_file.details'], body=None, + components=[form], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + if not file_chooser.file_path or not os.path.isfile(file_chooser.file_path): + watcher.request_confirmation(title=self.i18n['error'].capitalize(), + body=self.i18n['appimage.custom_action.install_file.invalid_file'], + deny_button=False) + elif not input_name.get_value() or not input_name.get_value().strip(): + watcher.request_confirmation(title=self.i18n['error'].capitalize(), + body=self.i18n['appimage.custom_action.install_file.invalid_name'], + deny_button=False) + else: + break + else: + return False + + appim = AppImage(i18n=self.i18n, imported=True) + appim.name = input_name.get_value().strip() + appim.local_file_path = file_chooser.file_path + appim.version = input_version.get_value() + appim.latest_version = input_version.get_value() + appim.description = input_description.get_value() + appim.categories = ['Imported'] + + if inp_cat.get_selected() != cat_ops[0].value: + appim.categories.append(inp_cat.get_selected()) + + installed = self.install(root_password=root_password, pkg=appim, watcher=watcher) + + if installed: + appim.installed = True + self.cache_to_disk(appim, None, False) + + return installed + + def update_file(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher): + file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}) + input_version = TextInputComponent(label=self.i18n['version'].capitalize()) + + while True: + if watcher.request_confirmation(title=self.i18n['appimage.custom_action.manual_update.details'], body=None, + components=[FormComponent(label='', components=[file_chooser, input_version], spaces=False)], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + if not file_chooser.file_path or not os.path.isfile(file_chooser.file_path): + watcher.request_confirmation(title=self.i18n['error'].capitalize(), + body=self.i18n['appimage.custom_action.install_file.invalid_file'], + deny_button=False) + else: + break + else: + return False + + pkg.local_file_path = file_chooser.file_path + pkg.version = input_version.get_value() + + 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) def _get_db_connection(self, db_path: str) -> sqlite3.Connection: if os.path.exists(db_path): @@ -82,7 +163,7 @@ class AppImageManager(SoftwareManager): found_map = {} idx = 0 for l in cursor.fetchall(): - app = AppImage(*l) + app = AppImage(*l, i18n=self.i18n) res.new.append(app) found_map[self._gen_app_key(app)] = {'app': app, 'idx': idx} idx += 1 @@ -118,7 +199,7 @@ class AppImageManager(SoftwareManager): for path in installed.split('\n'): if path: with open(path) as f: - app = AppImage(installed=True, **json.loads(f.read())) + app = AppImage(installed=True, i18n=self.i18n, **json.loads(f.read())) app.icon_url = app.icon_path res.installed.append(app) @@ -189,18 +270,25 @@ class AppImageManager(SoftwareManager): return False - def update(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: - if not self.uninstall(pkg, root_password, watcher): - watcher.show_message(title=self.i18n['error'], - body=self.i18n['appimage.error.uninstall_current_version'], - type_=MessageType.ERROR) - return False + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: + for req in requirements.to_upgrade: + watcher.change_status("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], req.pkg.name, req.pkg.version)) - if self.install(pkg, root_password, watcher): - self.cache_to_disk(pkg, None, False) - return True + if not self.uninstall(req.pkg, root_password, watcher): + watcher.show_message(title=self.i18n['error'], + body=self.i18n['appimage.error.uninstall_current_version'], + type_=MessageType.ERROR) + watcher.change_substatus('') + return False - return False + if not self.install(req.pkg, root_password, watcher): + watcher.change_substatus('') + return False + + self.cache_to_disk(req.pkg, None, False) + + watcher.change_substatus('') + return True def uninstall(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: if os.path.exists(pkg.get_disk_cache_path()): @@ -290,17 +378,47 @@ class AppImageManager(SoftwareManager): def install(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher) -> bool: handler = ProcessHandler(watcher) + out_dir = INSTALLATION_PATH + pkg.name.lower() + counter = 0 + while True: + if os.path.exists(out_dir): + self.logger.info("Installation dir '{}' already exists. Generating a different one".format(out_dir)) + out_dir += '-{}'.format(counter) + counter += 1 + else: + break + Path(out_dir).mkdir(parents=True, exist_ok=True) + pkg.install_dir = out_dir - appimage_url = pkg.url_download_latest_version if pkg.update else pkg.url_download - file_name = appimage_url.split('/')[-1] - pkg.version = pkg.latest_version - pkg.url_download = appimage_url + if pkg.imported: - file_path = out_dir + '/' + file_name - downloaded = self.file_downloader.download(file_url=pkg.url_download, watcher=watcher, - output_path=file_path, cwd=str(Path.home())) + downloaded, file_name = True, pkg.local_file_path.split('/')[-1] + + file_path = out_dir + '/' + file_name + + try: + moved, output = handler.handle_simple(SimpleProcess(['mv', pkg.local_file_path, file_path])) + except: + self.logger.error("Could not rename file '' as '{}'".format(pkg.local_file_path, file_path)) + moved = False + + if not moved: + watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['appimage.install.imported.rename_error'].format(bold(pkg.local_file_path.split('/')[-1]), bold(output)), + type_=MessageType.ERROR) + return False + + else: + appimage_url = pkg.url_download_latest_version if pkg.update else pkg.url_download + file_name = appimage_url.split('/')[-1] + pkg.version = pkg.latest_version + pkg.url_download = appimage_url + + file_path = out_dir + '/' + file_name + downloaded = self.file_downloader.download(file_url=pkg.url_download, watcher=watcher, + output_path=file_path, cwd=str(Path.home())) if downloaded: watcher.change_substatus(self.i18n['appimage.install.permission'].format(bold(file_name))) @@ -392,20 +510,19 @@ class AppImageManager(SoftwareManager): def requires_root(self, action: str, pkg: AppImage): return False - def _start_updater(self): + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): local_config = read_config(update_file=True) interval = local_config['db_updater']['interval'] or 20 * 60 - updater = DatabaseUpdater(http_client=self.context.http_client, logger=self.context.logger, + updater = DatabaseUpdater(task_man=task_manager, + i18n=self.context.i18n, + http_client=self.context.http_client, logger=self.context.logger, db_locks=self.db_locks, interval=interval) if local_config['db_updater']['enabled']: updater.start() - else: + elif internet_available: updater.download_databases() # only once - def prepare(self): - Thread(target=self._start_updater, daemon=True).start() - def list_updates(self, internet_available: bool) -> List[PackageUpdate]: res = self.read_installed(disk_loader=None, internet_available=internet_available) @@ -413,7 +530,7 @@ class AppImageManager(SoftwareManager): if res.installed: for app in res.installed: if app.update: - updates.append(PackageUpdate(pkg_id=app.name, pkg_type='appimage', version=app.latest_version)) + updates.append(PackageUpdate(pkg_id=app.name, pkg_type='AppImage', version=app.latest_version, name=app.name)) return updates @@ -458,7 +575,7 @@ class AppImageManager(SoftwareManager): cursor.execute(query.FIND_APPS_BY_NAME_FULL.format(','.join(["'{}'".format(s) for s in sugs_map.keys()]))) for t in cursor.fetchall(): - app = AppImage(*t) + app = AppImage(*t, i18n=self.i18n) res.append(PackageSuggestion(app, sugs_map[app.name.lower()])) self.logger.info("Mapped {} suggestions".format(len(res))) except: @@ -538,3 +655,22 @@ class AppImageManager(SoftwareManager): return True, None except: return False, [traceback.format_exc()] + + def get_custom_actions(self) -> List[CustomSoftwareAction]: + return self.custom_actions + + def get_upgrade_requirements(self, pkgs: List[AppImage], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements: + to_update = [] + + for pkg in pkgs: + requirement = UpgradeRequirement(pkg) + installed_size = self.http_client.get_content_length_in_bytes(pkg.url_download) + upgrade_size = self.http_client.get_content_length_in_bytes(pkg.url_download_latest_version) + requirement.required_size = upgrade_size + + if upgrade_size and installed_size: + requirement.extra_size = upgrade_size - installed_size + + to_update.append(requirement) + + return UpgradeRequirements([], [], to_update, []) diff --git a/bauh/gems/appimage/model.py b/bauh/gems/appimage/model.py index c17e52fe..4d70f1be 100644 --- a/bauh/gems/appimage/model.py +++ b/bauh/gems/appimage/model.py @@ -1,9 +1,18 @@ -from bauh.api.abstract.model import SoftwarePackage +from typing import List + +from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction from bauh.commons import resource from bauh.gems.appimage import ROOT_DIR, INSTALLATION_PATH +from bauh.view.util.translation import I18n CACHED_ATTRS = {'name', 'description', 'version', 'url_download', 'author', 'license', 'source', - 'icon_path', 'github', 'categories'} + 'icon_path', 'github', 'categories', 'imported', 'install_dir'} + +CUSTOM_ACTIONS = [CustomSoftwareAction(i18_label_key='appimage.custom_action.manual_update', + i18n_status_key='appimage.custom_action.manual_update.status', + manager_method='update_file', + requires_root=False, + icon_path=resource.get_path('img/refresh.svg', ROOT_DIR))] class AppImage(SoftwarePackage): @@ -11,7 +20,8 @@ class AppImage(SoftwarePackage): def __init__(self, name: str = None, description: str = None, github: 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, **kwargs): + url_download_latest_version: str = None, local_file_path: str = None, imported: bool = False, + i18n: I18n = None, install_dir: 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) @@ -23,6 +33,10 @@ class AppImage(SoftwarePackage): self.url_screenshot = url_screenshot self.author = author self.url_download_latest_version = url_download_latest_version + self.local_file_path = local_file_path + self.imported = imported + self.i18n = i18n + self.install_dir = install_dir def __repr__(self): return "{} (name={}, github={})".format(self.__class__.__name__, self.name, self.github) @@ -31,13 +45,13 @@ class AppImage(SoftwarePackage): return not self.installed and self.url_download def has_history(self): - return self.installed + return self.installed and not self.imported def has_info(self): return True def can_be_downgraded(self): - return self.installed + return self.installed and not self.imported def get_type(self): return 'AppImage' @@ -75,7 +89,9 @@ class AppImage(SoftwarePackage): return self.author def get_disk_cache_path(self) -> str: - if self.name: + if self.install_dir: + return self.install_dir + elif self.name: return INSTALLATION_PATH + self.name.lower() def get_disk_icon_path(self): @@ -83,3 +99,16 @@ class AppImage(SoftwarePackage): def has_screenshots(self): return not self.installed and self.url_screenshot + + def get_display_name(self) -> str: + if self.name and self.imported: + return '{} ( {} )'.format(self.name, self.i18n['imported']) + + return self.name + + def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: + if self.imported: + return CUSTOM_ACTIONS + + def supports_backup(self) -> bool: + return False diff --git a/bauh/gems/appimage/resources/img/refresh.svg b/bauh/gems/appimage/resources/img/refresh.svg new file mode 100755 index 00000000..16952792 --- /dev/null +++ b/bauh/gems/appimage/resources/img/refresh.svg @@ -0,0 +1,146 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca index b8e9429c..d0a0b757 100644 --- a/bauh/gems/appimage/resources/locale/ca +++ b/bauh/gems/appimage/resources/locale/ca @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Aplicacions publicades a https://appimage.github.io/ -appimage.install.permission=S’està concedint el permís d’execució a {} -appimage.install.extract=S’està extraient el contingut de {} -appimage.install.desktop_entry=S’està creant una drecera del menú -appimage.uninstall.error.remove_folder=No s’ha pogut suprimir el directori d’instal·lació de l’aplicació {} -appimage.info.url_download=URL del fitxer -appimage.info.icon_path=icona -appimage.history.0_version=versió -appimage.history.1_published_at=data -appimage.history.2_url_download=URL del fitxer -appimage.downgrade.impossible.title=No s’ha pogut revertir la versió -appimage.downgrade.impossible.body={} té només una versió publicada. -appimage.downgrade.unknown_version.body=No s’ha pogut identificar la versió actual de {} a l’historial de versions -appimage.downgrade.first_version={} és a la seva primera versió publicada -appimage.downgrade.uninstall_current_version=No s’ha pogut desinstal·lar la versió actual de {} -appimage.downgrade.install_version=No s’ha pogut instal·lar la versió {} ({}) -appimage.install.download.error=No s’ha pogut baixar el fitxer {}. El servidor del fitxer pot estar inactiu. -appimage.install.appimagelauncher.error = {appimgl} no permet la instal·lació de {app}. Desinstal·leu {appimgl}, reinicieu el sistema i torneu a provar d’instal·lar {app}. appimage.config.db_updates=Database update appimage.config.db_updates.activated=activated appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file +appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.custom_action.install_file=Install AppImage file +appimage.custom_action.install_file.details=Installation details +appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file +appimage.custom_action.install_file.invalid_name=You must define a name for the application +appimage.custom_action.install_file.status=Installing AppImage file +appimage.custom_action.manual_update=Upgrade file +appimage.custom_action.manual_update.details=Upgrade details +appimage.custom_action.manual_update.status=Upgrading +appimage.downgrade.first_version={} és a la seva primera versió publicada +appimage.downgrade.impossible.body={} té només una versió publicada. +appimage.downgrade.impossible.title=No s’ha pogut revertir la versió +appimage.downgrade.install_version=No s’ha pogut instal·lar la versió {} ({}) +appimage.downgrade.unknown_version.body=No s’ha pogut identificar la versió actual de {} a l’historial de versions +appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} +appimage.history.0_version=versió +appimage.history.1_published_at=data +appimage.history.2_url_download=URL del fitxer +appimage.info.icon_path=icona +appimage.info.imported.false=No +appimage.info.imported.true=Yes +appimage.info.install_dir=Installation directory +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}. +appimage.install.desktop_entry=S’està creant una drecera del menú +appimage.install.download.error=No s’ha pogut baixar el fitxer {}. El servidor del fitxer pot estar inactiu. +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.task.db_update=Actualització de bases de dades +appimage.uninstall.error.remove_folder=No s’ha pogut suprimir el directori d’instal·lació de l’aplicació {} +gem.appimage.info=Aplicacions publicades a https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de index 11fcbaa6..dba7dc48 100644 --- a/bauh/gems/appimage/resources/locale/de +++ b/bauh/gems/appimage/resources/locale/de @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Anwendungen veröffentlicht auf https://appimage.github.io/ -appimage.install.permission=Ausführberechtigungen für {} -appimage.install.extract=Entpacke Inhalt von {} -appimage.install.desktop_entry=Menü-Shortcut erstellen -appimage.uninstall.error.remove_folder=Anwendungordner {} konnte nicht entfernt werden -appimage.info.url_download=Datei URL -appimage.info.icon_path=icon -appimage.history.0_version=Version -appimage.history.1_published_at=Datum -appimage.history.2_url_download=Datei URL -appimage.downgrade.impossible.title=Downgrade nicht möglich -appimage.downgrade.impossible.body={} hat nur eine veröffentliche Version -appimage.downgrade.unknown_version.body=Die Identifikation der aktuellen Version von {} im Versionsverlauf war nicht möglich -appimage.downgrade.first_version={} ist in der ersten veröffentlichten Version -appimage.error.uninstall_current_version=Deinstallation der aktuellen Version von {} war nicht möglich -appimage.downgrade.install_version=Die Installation der Version {} ({}) war nicht möglich -appimage.install.download.error=Das Herunterladen der Datei {} ist fehlgeschlagen. Eventuell ist der Server nicht verfügbar -appimage.install.appimagelauncher.error={appimgl} verhindert die installation von {app}. Deinstalliere {appimgl}, starte deinen Computer neu und versuche die installation von {app} erneut. appimage.config.db_updates=Database update appimage.config.db_updates.activated=activated appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file +appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.custom_action.install_file=Install AppImage file +appimage.custom_action.install_file.details=Installation details +appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file +appimage.custom_action.install_file.invalid_name=You must define a name for the application +appimage.custom_action.install_file.status=Installing AppImage file +appimage.custom_action.manual_update=Upgrade file +appimage.custom_action.manual_update.details=Upgrade details +appimage.custom_action.manual_update.status=Upgrading +appimage.downgrade.first_version={} ist in der ersten veröffentlichten Version +appimage.downgrade.impossible.body={} hat nur eine veröffentliche Version +appimage.downgrade.impossible.title=Downgrade nicht möglich +appimage.downgrade.install_version=Die Installation der Version {} ({}) war nicht möglich +appimage.downgrade.unknown_version.body=Die Identifikation der aktuellen Version von {} im Versionsverlauf war nicht möglich +appimage.error.uninstall_current_version=Deinstallation der aktuellen Version von {} war nicht möglich +appimage.history.0_version=Version +appimage.history.1_published_at=Datum +appimage.history.2_url_download=Datei URL +appimage.info.icon_path=icon +appimage.info.imported.false=Nien +appimage.info.imported.true=Ya +appimage.info.install_dir=Installation directory +appimage.info.url_download=Datei URL +appimage.install.appimagelauncher.error={appimgl} verhindert die installation von {app}. Deinstalliere {appimgl}, starte deinen Computer neu und versuche die installation von {app} erneut. +appimage.install.desktop_entry=Menü-Shortcut erstellen +appimage.install.download.error=Das Herunterladen der Datei {} ist fehlgeschlagen. Eventuell ist der Server nicht verfügbar +appimage.install.extract=Entpacke Inhalt von {} +appimage.install.imported.rename_error=It was not possible to move the file {} to {} +appimage.install.permission=Ausführberechtigungen für {} +appimage.task.db_update=Updating databases +appimage.uninstall.error.remove_folder=Anwendungordner {} konnte nicht entfernt werden +gem.appimage.info=Anwendungen veröffentlicht auf https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en index 4e887779..93917a2c 100644 --- a/bauh/gems/appimage/resources/locale/en +++ b/bauh/gems/appimage/resources/locale/en @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Applications published at https://appimage.github.io/ -appimage.install.permission=Giving execution permission to {} -appimage.install.extract=Extracting the content from {} -appimage.install.desktop_entry=Generating a menu shortcut -appimage.uninstall.error.remove_folder=Could not remove the application installation directory {} -appimage.info.url_download=File URL -appimage.info.icon_path=icon -appimage.history.0_version=version -appimage.history.1_published_at=date -appimage.history.2_url_download=File URL -appimage.downgrade.impossible.title=Impossible to downgrade -appimage.downgrade.impossible.body={} has only one published version. -appimage.downgrade.unknown_version.body=It was not possible to identify {} current version in its versions history -appimage.downgrade.first_version={} is in its first published version -appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} -appimage.downgrade.install_version=It was not possible to install the version {} ({}) -appimage.install.download.error=It was not possible to download the file {}. The file server can be down. -appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again. appimage.config.db_updates=Database update appimage.config.db_updates.activated=activated appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications -appimage.config.db_updates.interval.tip=Update interval in SECONDS \ No newline at end of file +appimage.config.db_updates.interval.tip=Update interval in SECONDS +appimage.custom_action.install_file=Install AppImage file +appimage.custom_action.install_file.details=Installation details +appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file +appimage.custom_action.install_file.invalid_name=You must define a name for the application +appimage.custom_action.install_file.status=Installing AppImage file +appimage.custom_action.manual_update=Upgrade file +appimage.custom_action.manual_update.details=Upgrade details +appimage.custom_action.manual_update.status=Upgrading +appimage.downgrade.first_version={} is in its first published version +appimage.downgrade.impossible.body={} has only one published version. +appimage.downgrade.impossible.title=Impossible to downgrade +appimage.downgrade.install_version=It was not possible to install the version {} ({}) +appimage.downgrade.unknown_version.body=It was not possible to identify {} current version in its versions history +appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} +appimage.history.0_version=version +appimage.history.1_published_at=date +appimage.history.2_url_download=File URL +appimage.info.icon_path=icon +appimage.info.imported.false=No +appimage.info.imported.true=Yes +appimage.info.install_dir=Installation directory +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. +appimage.install.desktop_entry=Generating a menu shortcut +appimage.install.download.error=It was not possible to download the file {}. The file server can be down. +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.task.db_update=Updating databases +appimage.uninstall.error.remove_folder=Could not remove the application installation directory {} +gem.appimage.info=Applications published at https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es index a51d4dcc..e7c08115 100644 --- a/bauh/gems/appimage/resources/locale/es +++ b/bauh/gems/appimage/resources/locale/es @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Aplicativos publicados en https://appimage.github.io/ -appimage.install.permission=Concediendo permiso de ejecución a {} -appimage.install.extract=Extrayendo el contenido de {} -appimage.install.desktop_entry=Creando un atajo en el menú -appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {} -appimage.info.url_download=URL del archivo -appimage.info.icon_path=icono -appimage.history.0_version=versión -appimage.history.1_published_at=fecha -appimage.history.2_url_download=URL del archivo -appimage.downgrade.impossible.title=Imposible revertir la versión -appimage.downgrade.impossible.body={} solo tiene una versión publicada. -appimage.downgrade.unknown_version.body=No fue posible identificar la versión actual de {} en su historial de versiones -appimage.downgrade.first_version={} está en su primera versión publicada -appimage.downgrade.uninstall_current_version=No fue posible desinstalar la versión actual de {} -appimage.downgrade.install_version=No fue posible instalar la versión {} ({}) -appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo. -appimage.install.appimagelauncher.error={appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente. appimage.config.db_updates=Actualización de la base de datos appimage.config.db_updates.activated=activada appimage.config.db_updates.activated.tip=Será posible buscar actualizaciones relacionadas con las aplicaciones instaladas -appimage.config.db_updates.interval.tip=Intervalo de actualización en SEGUNDOS \ No newline at end of file +appimage.config.db_updates.interval.tip=Intervalo de actualización en SEGUNDOS +appimage.custom_action.install_file=Instalar archivo AppImage +appimage.custom_action.install_file.details=Detalles de instalación +appimage.custom_action.install_file.invalid_file=Debe definir un archivo AppImage válido +appimage.custom_action.install_file.invalid_name=Debe definir un nombre para la aplicación +appimage.custom_action.install_file.status=Instalando archivo AppImage +appimage.custom_action.manual_update=Actualizar archivo +appimage.custom_action.manual_update.details=Detalles de actualización +appimage.custom_action.manual_update.status=Actualizando +appimage.downgrade.first_version={} está en su primera versión publicada +appimage.downgrade.impossible.body={} solo tiene una versión publicada. +appimage.downgrade.impossible.title=Imposible revertir la versión +appimage.downgrade.install_version=No fue posible instalar la versión {} ({}) +appimage.downgrade.unknown_version.body=No fue posible identificar la versión actual de {} en su historial de versiones +appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} +appimage.history.0_version=versión +appimage.history.1_published_at=fecha +appimage.history.2_url_download=URL del archivo +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.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. +appimage.install.desktop_entry=Creando un atajo en el menú +appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo. +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.task.db_update=Actualizando base de datos +appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {} +gem.appimage.info=Aplicativos publicados en https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it index a42d6d5a..cfb5b42f 100644 --- a/bauh/gems/appimage/resources/locale/it +++ b/bauh/gems/appimage/resources/locale/it @@ -1,24 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Applicazioni pubblicate su https://appimage.github.io/ -appimage.install.permission=Gdare il permesso di esecuzione a {} -appimage.install.extract=Estrarre il contenuto da {} -appimage.install.desktop_entry=Genera un collegamento al menu -appimage.uninstall.error.remove_folder=Impossibile rimuovere la directory di installazione dell'applicazione {} -appimage.info.url_download=File URL -appimage.info.icon_path=icona -appimage.history.0_version=versione -appimage.history.1_published_at=data -appimage.history.2_url_download=File URL -appimage.downgrade.impossible.title=Impossibile eseguire il downgrade -appimage.downgrade.impossible.body={} ha solo una versione pubblicata. -appimage.downgrade.unknown_version.body=Non è stato possibile identificare la {} versione corrente nella sua cronologia delle versioni -appimage.downgrade.first_version={} è nella sua prima versione pubblicata -appimage.error.uninstall_current_version = Non è stato possibile disinstallare la versione corrente di {} -appimage.downgrade.install_version=Non è stato possibile installare la versione {} ({}) -appimage.install.download.error=Non è stato possibile scaricare il file {}. Il file server può essere inattivo. -appimage.install.appimagelauncher.error={appimgl} non consente l'installazione di {app}. Disinstallare {appimgl}, riavviare il sistema e provare a installare nuovamente {app}. appimage.config.db_updates=Database update appimage.config.db_updates.activated=activated appimage.config.db_updates.activated.tip=It will be possible to check for updates related to the installed applications appimage.config.db_updates.interval.tip=Update interval in SECONDS - +appimage.custom_action.install_file=Install AppImage file +appimage.custom_action.install_file.details=Installation details +appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file +appimage.custom_action.install_file.invalid_name=You must define a name for the application +appimage.custom_action.install_file.status=Installing AppImage file +appimage.custom_action.manual_update=Upgrade file +appimage.custom_action.manual_update.details=Upgrade details +appimage.custom_action.manual_update.status=Upgrading +appimage.downgrade.first_version={} è nella sua prima versione pubblicata +appimage.downgrade.impossible.body={} ha solo una versione pubblicata. +appimage.downgrade.impossible.title=Impossibile eseguire il downgrade +appimage.downgrade.install_version=Non è stato possibile installare la versione {} ({}) +appimage.downgrade.unknown_version.body=Non è stato possibile identificare la {} versione corrente nella sua cronologia delle versioni +appimage.error.uninstall_current_version=Non è stato possibile disinstallare la versione corrente di {} +appimage.history.0_version=versione +appimage.history.1_published_at=data +appimage.history.2_url_download=File URL +appimage.info.icon_path=icona +appimage.info.imported.false=No +appimage.info.imported.true=Yes +appimage.info.install_dir=Installation directory +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}. +appimage.install.desktop_entry=Genera un collegamento al menu +appimage.install.download.error=Non è stato possibile scaricare il file {}. Il file server può essere inattivo. +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.task.db_update=Aggiornamento dei database +appimage.uninstall.error.remove_folder=Impossibile rimuovere la directory di installazione dell'applicazione {} +gem.appimage.info=Applicazioni pubblicate su https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt index 376e5cbe..49e38871 100644 --- a/bauh/gems/appimage/resources/locale/pt +++ b/bauh/gems/appimage/resources/locale/pt @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Aplicativos publicados em https://appimage.github.io/ -appimage.install.permission=Concedendo permissão de execução para {} -appimage.install.extract=Extraindo o conteúdo de {} -appimage.install.desktop_entry=Criando um atalho no menu -appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {} -appimage.info.url_download=URL do arquivo -appimage.info.icon_path=ícone -appimage.history.0_version=versão -appimage.history.1_published_at=data -appimage.history.2_url_download=URL do arquivo -appimage.downgrade.impossible.title=Impossível reverter a versão -appimage.downgrade.impossible.body={} tem somente uma versão publicada. -appimage.downgrade.unknown_version.body=Não foi possível identificar a versão atual de {} no seu histórico de versões -appimage.downgrade.first_version={} se encontra em sua primeira versão publicada -appimage.downgrade.uninstall_current_version=Não foi possível desinstalar a versão atual de {} -appimage.downgrade.install_version=Não foi possivel instalar a versão {} ({}) -appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar. -appimage.install.appimagelauncher.error={appimgl} não está permitindo que o {app} seja instalado. Desinstale o {appimgl}, reinicie o sistema e tente instalar o {app} novamente. appimage.config.db_updates=Atualização da base de dados appimage.config.db_updates.activated=ativada appimage.config.db_updates.activated.tip=Será possível verificar se há atualizações para os aplicativos instalados -appimage.config.db_updates.interval.tip=Intervalo de atualização em SEGUNDOS \ No newline at end of file +appimage.config.db_updates.interval.tip=Intervalo de atualização em SEGUNDOS +appimage.custom_action.install_file=Instalar arquivo AppImage +appimage.custom_action.install_file.details=Detalhes de instalação +appimage.custom_action.install_file.invalid_file=Você precisa definir um arquivo AppImage válido +appimage.custom_action.install_file.invalid_name=Você precisa definir um nome para o aplicativo +appimage.custom_action.install_file.status=Instalando arquivo AppImage +appimage.custom_action.manual_update=Atualizar arquivo +appimage.custom_action.manual_update.details=Detalhes de atualização +appimage.custom_action.manual_update.status=Atualizando +appimage.downgrade.first_version={} se encontra em sua primeira versão publicada +appimage.downgrade.impossible.body={} tem somente uma versão publicada. +appimage.downgrade.impossible.title=Impossível reverter a versão +appimage.downgrade.install_version=Não foi possivel instalar a versão {} ({}) +appimage.downgrade.unknown_version.body=Não foi possível identificar a versão atual de {} no seu histórico de versões +appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {} +appimage.history.0_version=versão +appimage.history.1_published_at=data +appimage.history.2_url_download=URL do arquivo +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.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. +appimage.install.desktop_entry=Criando um atalho no menu +appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar. +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.task.db_update=Atualizando base de dados +appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {} +gem.appimage.info=Aplicativos publicados em https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/resources/locale/ru b/bauh/gems/appimage/resources/locale/ru index 56a75f50..debb5925 100644 --- a/bauh/gems/appimage/resources/locale/ru +++ b/bauh/gems/appimage/resources/locale/ru @@ -1,23 +1,36 @@ -gem.appimage.label=AppImage -gem.appimage.info=Приложения, опубликованные на https://appimage.github.io/ -appimage.install.permission=Установить права на выполнение для {} -appimage.install.extract=Извлечь содержимое из {} -appimage.install.desktop_entry=Создать ярлык в меню -appimage.uninstall.error.remove_folder=Не удалось удалить каталог приложения {} -appimage.info.url_download=URL Файла -appimage.info.icon_path=Значок -appimage.history.0_version=версия -appimage.history.1_published_at=дата -appimage.history.2_url_download=URL Файла -appimage.downgrade.impossible.title=Не удалось понизить версию -appimage.downgrade.impossible.body={} Имеет только одну опубликованную версию -appimage.downgrade.unknown_version.body=Не удалось определить текущую версию {} в истории версий -appimage.downgrade.first_version={} первая опубликованная версия -appimage.error.uninstall_current_version=Не удалось удалить текущую версию {} -appimage.downgrade.install_version=Не удалось установить версию {} ({}) -appimage.install.download.error=Не удалось загрузить файл {}. Файловый сервер не отвечает -appimage.install.appimagelauncher.error={appimgl} не позволяет установить {app} . Удалите {appimgl}, перезагрузите ваш компьютер и попробуйте снова установить {app} . appimage.config.db_updates=Обновление базы данных appimage.config.db_updates.activated=активировать appimage.config.db_updates.activated.tip=Позволит проверять обновления установленных приложений appimage.config.db_updates.interval.tip=Интервал обновления в секундах +appimage.custom_action.install_file=Install AppImage file +appimage.custom_action.install_file.details=Installation details +appimage.custom_action.install_file.invalid_file=You must define a valid AppImage file +appimage.custom_action.install_file.invalid_name=You must define a name for the application +appimage.custom_action.install_file.status=Installing AppImage file +appimage.custom_action.manual_update=Upgrade file +appimage.custom_action.manual_update.details=Upgrade details +appimage.custom_action.manual_update.status=Upgrading +appimage.downgrade.first_version={} первая опубликованная версия +appimage.downgrade.impossible.body={} Имеет только одну опубликованную версию +appimage.downgrade.impossible.title=Не удалось понизить версию +appimage.downgrade.install_version=Не удалось установить версию {} ({}) +appimage.downgrade.unknown_version.body=Не удалось определить текущую версию {} в истории версий +appimage.error.uninstall_current_version=Не удалось удалить текущую версию {} +appimage.history.0_version=версия +appimage.history.1_published_at=дата +appimage.history.2_url_download=URL Файла +appimage.info.icon_path=Значок +appimage.info.imported.false=No +appimage.info.imported.true=Yes +appimage.info.install_dir=Installation directory +appimage.info.url_download=URL Файла +appimage.install.appimagelauncher.error={appimgl} не позволяет установить {app} . Удалите {appimgl}, перезагрузите ваш компьютер и попробуйте снова установить {app} . +appimage.install.desktop_entry=Создать ярлык в меню +appimage.install.download.error=Не удалось загрузить файл {}. Файловый сервер не отвечает +appimage.install.extract=Извлечь содержимое из {} +appimage.install.imported.rename_error=It was not possible to move the file {} to {} +appimage.install.permission=Установить права на выполнение для {} +appimage.task.db_update=Updating databases +appimage.uninstall.error.remove_folder=Не удалось удалить каталог приложения {} +gem.appimage.info=Приложения, опубликованные на https://appimage.github.io/ +gem.appimage.label=AppImage \ No newline at end of file diff --git a/bauh/gems/appimage/worker.py b/bauh/gems/appimage/worker.py index 57cbeb45..5e494d00 100644 --- a/bauh/gems/appimage/worker.py +++ b/bauh/gems/appimage/worker.py @@ -9,33 +9,54 @@ from threading import Thread import requests +from bauh.api.abstract.handler import TaskManager from bauh.api.http import HttpClient from bauh.commons import internet -from bauh.gems.appimage import LOCAL_PATH +from bauh.gems.appimage import LOCAL_PATH, get_icon_path +from bauh.view.util.translation import I18n class DatabaseUpdater(Thread): URL_DB = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/appimage/dbs.tar.gz' COMPRESS_FILE_PATH = LOCAL_PATH + '/db.tar.gz' - def __init__(self, http_client: HttpClient, logger: logging.Logger, db_locks: dict, interval: int): + def __init__(self, task_man: TaskManager, i18n: I18n, http_client: HttpClient, logger: logging.Logger, db_locks: dict, interval: int): super(DatabaseUpdater, self).__init__(daemon=True) self.http_client = http_client self.logger = logger self.db_locks = db_locks self.sleep = interval + self.i18n = i18n + self.task_man = task_man + self.task_id = 'appim_db' + + def _finish_task(self): + if self.task_man: + self.task_man.update_progress(self.task_id, 100, None) + self.task_man.finish_task(self.task_id) + self.task_man = None def download_databases(self): + if self.task_man: + self.task_man.register_task(self.task_id, self.i18n['appimage.task.db_update'], get_icon_path()) + self.task_man.update_progress(self.task_id, 10, None) + try: - if not internet.is_available(self.http_client, self.logger): + if not internet.is_available(): + self._finish_task() return except requests.exceptions.ConnectionError: self.logger.warning('The internet connection seems to be off.') + self._finish_task() return self.logger.info('Retrieving AppImage databases') - res = self.http_client.get(self.URL_DB) + try: + res = self.http_client.get(self.URL_DB, session=False) + except Exception as e: + self.logger.error("An error ocurred while downloading the AppImage database: {}".format(e.__class__.__name__)) + res = None if res: Path(LOCAL_PATH).mkdir(parents=True, exist_ok=True) @@ -71,9 +92,10 @@ class DatabaseUpdater(Thread): self.logger.info('Deleting {}'.format(self.COMPRESS_FILE_PATH)) os.remove(self.COMPRESS_FILE_PATH) self.logger.info('Successfully removed {}'.format(self.COMPRESS_FILE_PATH)) - + self._finish_task() else: self.logger.warning('Could not download the database file {}'.format(self.URL_DB)) + self._finish_task() def run(self): while True: diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 637484d0..98117b48 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -2,15 +2,23 @@ import os from pathlib import Path from bauh.api.constants import CACHE_PATH, CONFIG_PATH, TEMP_DIR +from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -BUILD_DIR = '{}/aur'.format(TEMP_DIR) +BUILD_DIR = '{}/arch'.format(TEMP_DIR) ARCH_CACHE_PATH = CACHE_PATH + '/arch' -CATEGORIES_CACHE_DIR = ARCH_CACHE_PATH + '/categories' -CATEGORIES_FILE_PATH = CATEGORIES_CACHE_DIR + '/aur.txt' -URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt' +CATEGORIES_FILE_PATH = ARCH_CACHE_PATH + '/categories.txt' +URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/categories.txt' CONFIG_DIR = '{}/.config/bauh/arch'.format(Path.home()) CUSTOM_MAKEPKG_FILE = '{}/makepkg.conf'.format(CONFIG_DIR) -AUR_INDEX_FILE = '{}/aur.txt'.format(BUILD_DIR) +AUR_INDEX_FILE = '{}/arch.txt'.format(BUILD_DIR) CONFIG_FILE = '{}/arch.yml'.format(CONFIG_PATH) -SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/suggestions.txt' +SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/aur_suggestions.txt' + + +def get_icon_path() -> str: + return resource.get_path('img/arch.svg', ROOT_DIR) + + +def get_repo_icon_path() -> str: + return resource.get_path('img/repo.svg', ROOT_DIR) diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 253f3801..3947f0c7 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -1,17 +1,19 @@ import logging import os import re -from typing import Set, List +import urllib.parse +from typing import Set, List, Iterable, Dict + +import requests from bauh.api.http import HttpClient -import urllib.parse - from bauh.gems.arch import pacman, AUR_INDEX_FILE from bauh.gems.arch.exceptions import PackageNotFoundException URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&' URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=' URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg=' +URL_INDEX = 'https://aur.archlinux.org/packages.gz' RE_SRCINFO_KEYS = re.compile(r'(\w+)\s+=\s+(.+)\n') RE_SPLIT_DEP = re.compile(r'[<>]?=') @@ -33,7 +35,9 @@ KNOWN_LIST_FIELDS = ('validpgpkeys', 'source_i686', 'makedepends', 'makedepends_x86_64', - 'makedepends_i686') + 'makedepends_i686', + 'provides', + 'conflicts') def map_pkgbuild(pkgbuild: str) -> dict: @@ -70,19 +74,30 @@ class AURClient: self.http_client = http_client self.logger = logger self.x86_64 = x86_64 + self.srcinfo_cache = {} def search(self, words: str) -> dict: return self.http_client.get_json(URL_SEARCH + words) - def get_info(self, names: Set[str]) -> List[dict]: + def get_info(self, names: Iterable[str]) -> List[dict]: res = self.http_client.get_json(URL_INFO + self._map_names_as_queries(names)) return res['results'] if res and res.get('results') else [] def get_src_info(self, name: str) -> dict: + srcinfo = self.srcinfo_cache.get(name) + + if srcinfo: + return srcinfo + res = self.http_client.get(URL_SRC_INFO + urllib.parse.quote(name)) if res and res.text: - return map_srcinfo(res.text) + srcinfo = map_srcinfo(res.text) + + if srcinfo: + self.srcinfo_cache[name] = srcinfo + + return srcinfo self.logger.warning('No .SRCINFO found for {}'.format(name)) self.logger.info('Checking if {} is based on another package'.format(name)) @@ -96,7 +111,12 @@ class AURClient: info_base = info.get('PackageBase') if info_name and info_base and info_name != info_base: self.logger.info('{p} is based on {b}. Retrieving {b} .SRCINFO'.format(p=info_name, b=info_base)) - return self.get_src_info(info_base) + srcinfo = self.get_src_info(info_base) + + if srcinfo: + self.srcinfo_cache[name] = srcinfo + + return srcinfo def extract_required_dependencies(self, srcinfo: dict) -> Set[str]: deps = set() @@ -123,7 +143,7 @@ class AURClient: return '&'.join(['arg[{}]={}'.format(i, urllib.parse.quote(n)) for i, n in enumerate(names)]) def read_local_index(self) -> dict: - self.logger.info('Checking if the AUR index file exists') + self.logger.info('Checking if the cached AUR index file exists') if os.path.exists(AUR_INDEX_FILE): self.logger.info('Reading AUR index file from {}'.format(AUR_INDEX_FILE)) index = {} @@ -135,3 +155,57 @@ class AURClient: self.logger.info("AUR index file read") return index self.logger.warning('The AUR index file was not found') + + def download_names(self) -> Set[str]: + self.logger.info('Downloading AUR index') + try: + res = self.http_client.get(URL_INDEX) + + if res and res.text: + return {n.strip() for n in res.text.split('\n') if n and not n.startswith('#')} + else: + self.logger.warning('No data returned from: {}'.format(URL_INDEX)) + except requests.exceptions.ConnectionError: + self.logger.warning('No internet connection: could not pre-index packages') + + self.logger.info("Finished") + + def read_index(self) -> Iterable[str]: + index = self.read_local_index() + + if not index: + self.logger.warning("Cached AUR index file not found") + pkgnames = self.download_names() + + if pkgnames: + return pkgnames + else: + self.logger.warning("Could not load AUR index on the context") + else: + return index.values() + + def clean_caches(self): + self.srcinfo_cache.clear() + + def map_update_data(self, pkgname: str, latest_version: str, srcinfo: dict = None) -> dict: + info = self.get_src_info(pkgname) if not srcinfo else srcinfo + + provided = set() + provided.add(pkgname) + + if info: + provided.add('{}={}'.format(pkgname, info['pkgver'])) + if info.get('provides'): + provided.update(info.get('provides')) + + return {'c': info.get('conflicts'), 's': None, 'p': provided, 'r': 'arch', + 'v': info['pkgver'], 'd': self.extract_required_dependencies(info)} + else: + if latest_version: + provided.add('{}={}'.format(pkgname, latest_version)) + + return {'c': None, 's': None, 'p': provided, 'r': 'arch', 'v': latest_version, 'd': set()} + + def fill_update_data(self, output: Dict[str, dict], pkgname: str, latest_version: str, srcinfo: dict = None): + data = self.map_update_data(pkgname=pkgname, latest_version=latest_version, srcinfo=srcinfo) + output[pkgname] = data diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 31cad670..f7d37c88 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -3,5 +3,12 @@ from bauh.gems.arch import CONFIG_FILE def read_config(update_file: bool = False) -> dict: - template = {'optimize': True, 'transitive_checking': True, "sync_databases": True, "simple_checking": False} + template = {'optimize': True, + "sync_databases": True, + "clean_cached": True, + 'arch': True, + 'repositories': True, + "refresh_mirrors_startup": False, + "sync_databases_startup": True, + 'mirrors_sort_limit': 5} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/confirmation.py b/bauh/gems/arch/confirmation.py index a34e1d44..a7d07e5f 100644 --- a/bauh/gems/arch/confirmation.py +++ b/bauh/gems/arch/confirmation.py @@ -1,23 +1,33 @@ -from typing import Set, List, Tuple +from typing import Set, List, Tuple, Dict from bauh.api.abstract.handler import ProcessWatcher -from bauh.api.abstract.view import MultipleSelectComponent, InputOption +from bauh.api.abstract.view import MultipleSelectComponent, InputOption, FormComponent, SingleSelectComponent, \ + SelectViewType from bauh.commons import resource from bauh.commons.html import bold -from bauh.gems.arch import ROOT_DIR +from bauh.commons.system import get_human_size_str +from bauh.gems.arch import ROOT_DIR, get_repo_icon_path, get_icon_path, pacman from bauh.view.util.translation import I18n -def _get_mirror_icon(mirror: str): - return resource.get_path('img/{}.svg'.format('arch' if mirror == 'aur' else 'mirror'), ROOT_DIR) +def _get_repo_icon(repository: str): + return resource.get_path('img/{}.svg'.format('arch' if repository == 'arch' else 'repo'), ROOT_DIR) -def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: I18n) -> Set[str]: +def request_optional_deps(pkgname: str, pkg_repos: dict, watcher: ProcessWatcher, i18n: I18n) -> Set[str]: opts = [] - for p, d in pkg_mirrors.items(): - op = InputOption('{}{} ( {}: {} )'.format(p, ': ' + d['desc'] if d['desc'] else '', i18n['repository'], d['mirror'].upper()), p) - op.icon_path = _get_mirror_icon(d['mirror']) + repo_deps = [p for p, data in pkg_repos.items() if data['repository'] != 'arch'] + sizes = pacman.get_update_size(repo_deps) if repo_deps else None + + for p, d in pkg_repos.items(): + size = sizes.get(p) + op = InputOption('{}{} ( {}: {} ) - {}: {}'.format(p, ': ' + d['desc'] if d['desc'] else '', + i18n['repository'], + d['repository'].upper(), + i18n['size'].capitalize(), + get_human_size_str(size) if size else '?'), p) + op.icon_path = _get_repo_icon(d['repository']) opts.append(op) view_opts = MultipleSelectComponent(label='', @@ -39,15 +49,56 @@ def request_install_missing_deps(pkgname: str, deps: List[Tuple[str, str]], watc opts = [] - sorted_deps = [*deps] - sorted_deps.sort(key=lambda e: e[0]) + repo_deps = [d[0] for d in deps if d[1] != 'arch'] + sizes = pacman.get_update_size(repo_deps) if repo_deps else None - for dep in sorted_deps: - op = InputOption('{} ( {}: {} )'.format(dep[0], i18n['repository'], dep[1].upper()), dep[0]) + for dep in deps: + size = sizes.get(dep[0]) + op = InputOption('{} ( {}: {} ) - {}: {}'.format(dep[0], + i18n['repository'], + dep[1].upper(), + i18n['size'].capitalize(), + get_human_size_str(size) if size else '?'), dep[0]) op.read_only = True - op.icon_path = _get_mirror_icon(dep[1]) + op.icon_path = _get_repo_icon(dep[1]) opts.append(op) comp = MultipleSelectComponent(label='', options=opts, default_options=set(opts)) - return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg, [comp], confirmation_label=i18n['continue'].capitalize(), deny_label=i18n['cancel'].capitalize()) + + +def request_providers(providers_map: Dict[str, Set[str]], repo_map: Dict[str, str], watcher: ProcessWatcher, i18n: I18n) -> Set[str]: + msg = "

{}.

{}.

".format(i18n['arch.dialog.providers.line1'], + i18n['arch.dialog.providers.line2']) + + repo_icon_path = get_repo_icon_path() + aur_icon_path = get_icon_path() + + form = FormComponent([], label='') + + for dep, providers in providers_map.items(): + opts = [] + + providers_list = [*providers] + providers_list.sort() + + for p in providers_list: + repo = repo_map.get(p, 'arch') + opts.append(InputOption(label=p, + value=p, + icon_path=aur_icon_path if repo == 'arch' else repo_icon_path, + tooltip='{}: {}'.format(i18n['repository'].capitalize(), repo))) + + form.components.append(SingleSelectComponent(label=bold(dep.lower()), + options=opts, + default_option=opts[0], + type_=SelectViewType.COMBO, + max_per_line=1)) + + if watcher.request_confirmation(title=i18n['arch.providers'].capitalize(), + body=msg, + components=[form], + confirmation_label=i18n['proceed'].capitalize(), + deny_label=i18n['cancel'].capitalize()): + + return {s.get_selected() for s in form.components} diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index a698415c..7a38319c 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1,25 +1,27 @@ +import glob import json import os import re import shutil import subprocess +import tarfile import time import traceback from datetime import datetime from math import floor from pathlib import Path from threading import Thread -from typing import List, Set, Type, Tuple, Dict +from typing import List, Set, Type, Tuple, Dict, Iterable import requests -from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext +from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ - SuggestionPriority + SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ - ViewComponent, PanelComponent + ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent from bauh.api.constants import TEMP_DIR from bauh.commons import user from bauh.commons.category import CategoriesDownloader @@ -27,27 +29,122 @@ from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ SimpleProcess -from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, \ - gpg, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ - CONFIG_FILE +from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, message, confirmation, disk, git, \ + gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ + CONFIG_FILE, get_icon_path, database, mirrors, sorting, cpu_manager, ARCH_CACHE_PATH from bauh.gems.arch.aur import AURClient from bauh.gems.arch.config import read_config -from bauh.gems.arch.depedencies import DependenciesAnalyser +from bauh.gems.arch.dependencies import DependenciesAnalyser +from bauh.gems.arch.exceptions import PackageNotFoundException from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.model import ArchPackage -from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer +from bauh.gems.arch.output import TransactionStatusHandler +from bauh.gems.arch.updates import UpdatesSummarizer +from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, SyncDatabases, \ + RefreshMirrors URL_GIT = 'https://aur.archlinux.org/{}.git' URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz' URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=' -RE_SPLIT_VERSION = re.compile(r'(=|>|<)') +RE_SPLIT_VERSION = re.compile(r'([=><]+)') SOURCE_FIELDS = ('source', 'source_x86_64') RE_PRE_DOWNLOAD_WL_PROTOCOLS = re.compile(r'^(.+::)?(https?|ftp)://.+') RE_PRE_DOWNLOAD_BL_EXT = re.compile(r'.+\.(git|gpg)$') +class TransactionContext: + + def __init__(self, name: str = None, base: str = None, maintainer: str = None, watcher: ProcessWatcher = None, + handler: ProcessHandler = None, dependency: bool = None, skip_opt_deps: bool = False, root_password: str = None, + build_dir: str = None, project_dir: str = None, change_progress: bool = False, arch_config: dict = None, + install_file: str = None, repository: str = None, pkg: ArchPackage = None, + remote_repo_map: Dict[str, str] = None, provided_map: Dict[str, Set[str]] = None, + remote_provided_map: Dict[str, Set[str]] = None, aur_idx: Set[str] = None, + missing_deps: List[Tuple[str, str]] = None): + self.name = name + self.base = base + self.maintainer = maintainer + self.watcher = watcher + self.handler = handler + self.dependency = dependency + self.skip_opt_deps = skip_opt_deps + self.build_dir = build_dir + self.project_dir = project_dir + self.root_password = root_password + self.change_progress = change_progress + self.repository = repository + self.config = arch_config + self.install_file = install_file + self.pkg = pkg + self.provided_map = provided_map + self.remote_repo_map = remote_repo_map + self.remote_provided_map = remote_provided_map + self.aur_idx = aur_idx + self.missing_deps = missing_deps + + @classmethod + def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: str, handler: ProcessHandler) -> "TransactionContext": + return cls(name=pkg.name, base=pkg.get_base_name(), maintainer=pkg.maintainer, repository=pkg.repository, + arch_config=arch_config, watcher=handler.watcher, handler=handler, skip_opt_deps=True, + change_progress=True, root_password=root_password, dependency=False) + + def get_base_name(self): + return self.base if self.base else self.name + + def get_project_dir(self): + return self.project_dir or '.' + + def clone_base(self): + return TransactionContext(watcher=self.watcher, handler=self.handler, root_password=self.root_password, + arch_config=self.config) + + def gen_dep_context(self, name: str, repository: str): + dep_context = self.clone_base() + dep_context.name = name + dep_context.repository = repository + dep_context.dependency = True + dep_context.change_progress = False + return dep_context + + def has_install_file(self) -> bool: + return self.install_file is not None + + def get_package_path(self) -> str: + return self.install_file if self.install_file else self.name + + def get_version(self) -> str: + return self.pkg.version if self.pkg else None + + def get_aur_idx(self, aur_client: AURClient) -> Set[str]: + if self.aur_idx is None: + if bool(self.config['arch']): + self.aur_idx = aur_client.read_index() + else: + self.aur_idx = set() + + return self.aur_idx + + def get_provided_map(self) -> Dict[str, Set[str]]: + if self.provided_map is None: + self.provided_map = pacman.map_provided() + + return self.provided_map + + def get_remote_provided_map(self) -> Dict[str, Set[str]]: + if self.remote_provided_map is None: + self.remote_provided_map = pacman.map_provided(remote=True) + + return self.remote_provided_map + + def get_remote_repo_map(self) -> Dict[str, str]: + if self.remote_repo_map is None: + self.remote_repo_map = pacman.map_repositories() + + return self.remote_repo_map + + class ArchManager(SoftwareManager): def __init__(self, context: ApplicationContext): @@ -58,22 +155,134 @@ class ArchManager(SoftwareManager): self.mapper = ArchDataMapper(http_client=context.http_client, i18n=context.i18n) self.i18n = context.i18n self.aur_client = AURClient(http_client=context.http_client, logger=context.logger, x86_64=context.is_system_x86_64()) - self.dcache_updater = ArchDiskCacheUpdater(context.logger, context.disk_cache) + self.dcache_updater = None self.logger = context.logger self.enabled = True self.arch_distro = context.distro == 'arch' - self.categories_mapper = CategoriesDownloader('AUR', context.http_client, context.logger, self, self.context.disk_cache, - URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH) self.categories = {} - self.deps_analyser = DependenciesAnalyser(self.aur_client) - self.local_config = None + self.deps_analyser = DependenciesAnalyser(self.aur_client, self.i18n) self.http_client = context.http_client + self.custom_actions = { + 'sys_up': CustomSoftwareAction(i18_label_key='arch.custom_action.upgrade_system', + i18n_status_key='arch.custom_action.upgrade_system.status', + manager_method='upgrade_system', + icon_path=get_icon_path(), + requires_root=True, + backup=True, + manager=self), + 'ref_dbs': CustomSoftwareAction(i18_label_key='arch.custom_action.refresh_dbs', + i18n_status_key='arch.sync_databases.substatus', + manager_method='sync_databases', + icon_path=get_icon_path(), + requires_root=True, + manager=self), + 'ref_mirrors': CustomSoftwareAction(i18_label_key='arch.custom_action.refresh_mirrors', + i18n_status_key='arch.task.mirrors', + manager_method='refresh_mirrors', + icon_path=get_icon_path(), + requires_root=True, + manager=self), + 'clean_cache': CustomSoftwareAction(i18_label_key='arch.custom_action.clean_cache', + i18n_status_key='arch.custom_action.clean_cache.status', + manager_method='clean_cache', + icon_path=get_icon_path(), + requires_root=True, + manager=self) + } + self.index_aur = None - def get_semantic_search_map(self) -> Dict[str, str]: + @staticmethod + def get_semantic_search_map() -> Dict[str, str]: return {'google chrome': 'google-chrome', 'chrome google': 'google-chrome', 'googlechrome': 'google-chrome'} + def refresh_mirrors(self, root_password: str, watcher: ProcessWatcher) -> bool: + handler = ProcessHandler(watcher) + + if self._is_database_locked(handler, root_password): + return False + + available_countries = pacman.list_mirror_countries() + current_countries = pacman.get_current_mirror_countries() + + if not available_countries: + self.logger.warning("No country available") + countries = current_countries + else: + country_opts = [InputOption(label=self.i18n['arch.custom_action.refresh_mirrors.location.all'], value='all', + tooltip=self.i18n['arch.custom_action.refresh_mirrors.location.all.tip'])] + mapped_opts = [InputOption(label=' '.join((w.capitalize() for w in self.i18n[' '.join(c.split('_'))].split(' '))), + value=c) for c in available_countries] + mapped_opts.sort(key=lambda o: o.label) + + if len(current_countries) == 1 and current_countries[0] == 'all': + default_opts = {country_opts[0]} + else: + default_opts = {o for o in mapped_opts if o.value in current_countries} + country_opts.extend(default_opts) + + country_opts.extend((o for o in mapped_opts if o not in default_opts)) + + select = MultipleSelectComponent(options=country_opts, + default_options=default_opts, + max_per_line=3, + label=self.i18n['arch.custom_action.refresh_mirrors.select_label']) + + if watcher.request_confirmation(title=self.i18n['arch.custom_action.refresh_mirrors'], + body=None, + components=[select], + confirmation_label=self.i18n['continue'].capitalize(), + deny_label=self.i18n["cancel"].capitalize()): + countries = select.get_selected_values() + + if 'all' in countries or len(countries) == len(available_countries): + countries = ['all'] + else: + watcher.print("Aborted by the user") + return False + + watcher.change_substatus(self.i18n['arch.custom_action.refresh_mirrors.status.updating']) + + if current_countries == countries: + success, output = handler.handle_simple(pacman.refresh_mirrors(root_password)) + else: + success, output = handler.handle_simple(pacman.update_mirrors(root_password, countries)) + + if not success: + watcher.show_message(title=self.i18n["action.failed"].capitalize(), + body=self.i18n['arch.custom_action.refresh_mirrors.failed'], + type_=MessageType.ERROR) + return False + + sort_limit = read_config()['mirrors_sort_limit'] + + if sort_limit is not None and isinstance(sort_limit, int) and sort_limit >= 0: + watcher.change_substatus(self.i18n['arch.custom_action.refresh_mirrors.status.sorting']) + handler.handle_simple(pacman.sort_fastest_mirrors(root_password, sort_limit)) + + mirrors.register_sync(self.logger) + + watcher.change_substatus(self.i18n['arch.sync_databases.substatus']) + return self.sync_databases(root_password=root_password, watcher=watcher) + + def sync_databases(self, root_password: str, watcher: ProcessWatcher) -> bool: + handler = ProcessHandler(watcher) + + if self._is_database_locked(handler, root_password): + return False + + success, output = handler.handle_simple(pacman.sync_databases(root_password, force=True)) + + if not success: + watcher.show_message(title=self.i18n["action.failed"].capitalize(), + body=self.i18n['arch.custom_action.refresh_mirrors.failed'], + type_=MessageType.ERROR) + return False + + database.register_sync(self.logger) + return True + def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader): app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories) app.downgrade_enabled = downgrade_enabled @@ -88,28 +297,45 @@ class ArchManager(SoftwareManager): Thread(target=self.mapper.fill_package_build, args=(app,), daemon=True).start() - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: - if is_url: - return SearchResult([], [], 0) + def _search_in_repos_and_fill(self, words: str, disk_loader: DiskCacheLoader, read_installed: Thread, installed: dict, res: SearchResult): + repo_search = pacman.search(words) - downgrade_enabled = git.is_enabled() - res = SearchResult([], [], 0) + if repo_search: + repo_pkgs = [] + for name, data in repo_search.items(): + pkg = ArchPackage(name=name, i18n=self.i18n, **data) + pkg.downgrade_enabled = True + repo_pkgs.append(pkg) - installed = {} - read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()), daemon=True) - read_installed.start() + if repo_pkgs: + read_installed.join() - mapped_words = self.get_semantic_search_map().get(words) + for pkg in repo_pkgs: + if installed['signed'] and pkg.name in installed['signed']: + pkg.installed = True - api_res = self.aur_client.search(mapped_words if mapped_words else words) + if disk_loader: + disk_loader.fill(pkg) + + res.installed.append(pkg) + else: + pkg.installed = False + res.new.append(pkg) + + def _search_in_aur_and_fill(self, words: str, disk_loader: DiskCacheLoader, read_installed: Thread, installed: dict, res: SearchResult): + api_res = self.aur_client.search(words) if api_res and api_res.get('results'): read_installed.join() + downgrade_enabled = git.is_enabled() for pkgdata in api_res['results']: self._upgrade_search_result(pkgdata, installed, downgrade_enabled, res, disk_loader) else: # if there are no results from the API (it could be because there were too many), tries the names index: + if self.index_aur: + self.index_aur.join() + aur_index = self.aur_client.read_local_index() if aur_index: self.logger.info("Querying through the local AUR index") @@ -125,10 +351,44 @@ class ArchManager(SoftwareManager): if pkgsinfo: read_installed.join() + downgrade_enabled = git.is_enabled() for pkgdata in pkgsinfo: self._upgrade_search_result(pkgdata, installed, downgrade_enabled, res, disk_loader) + def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: + if is_url: + return SearchResult([], [], 0) + + arch_config = read_config() + + if not any([arch_config['repositories'], arch_config['arch']]): + return SearchResult([], [], 0) + + installed = {} + read_installed = Thread(target=lambda: installed.update(pacman.map_installed(repositories=bool(arch_config['repositories']), + aur=bool(arch_config['arch']))), daemon=True) + read_installed.start() + + res = SearchResult([], [], 0) + + if not any((arch_config['arch'], arch_config['repositories'])): + return res + + mapped_words = self.get_semantic_search_map().get(words) + final_words = mapped_words or words + + aur_search = None + if arch_config['arch']: + aur_search = Thread(target=self._search_in_aur_and_fill, args=(final_words, disk_loader, read_installed, installed, res), daemon=True) + aur_search.start() + + if arch_config['repositories']: + self._search_in_repos_and_fill(final_words, disk_loader, read_installed, installed, res) + + if aur_search: + aur_search.join() + res.total = len(res.installed) + len(res.new) return res @@ -158,7 +418,7 @@ class ArchManager(SoftwareManager): for name, data in not_signed.items(): pkg = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'), - installed=True, mirror='aur', i18n=self.i18n) + installed=True, repository='arch', i18n=self.i18n) pkg.categories = self.categories.get(pkg.name) pkg.downgrade_enabled = downgrade_enabled @@ -169,57 +429,93 @@ class ArchManager(SoftwareManager): pkgs.append(pkg) - def _fill_mirror_pkgs(self, mirrors: dict, apps: list): - # TODO - for name, data in mirrors.items(): - app = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'), i18n=self.i18n) - app.installed = True - app.mirror = '' # TODO - app.update = False # TODO - apps.append(app) + def _fill_repo_updates(self, updates: dict): + updates.update(pacman.list_repository_updates()) + + def _fill_repo_pkgs(self, signed: dict, pkgs: list, disk_loader: DiskCacheLoader): + updates = {} + + thread_updates = Thread(target=self._fill_repo_updates, args=(updates,), daemon=True) + thread_updates.start() + + repo_map = pacman.map_repositories(list(signed.keys())) + + if len(repo_map) != len(signed): + self.logger.warning("Not mapped all signed packages repositories. Mapped: {}. Total: {}".format(len(repo_map), len(signed))) + + thread_updates.join() + + self.logger.info("Repository updates found" if updates else "No repository updates found") + + for name, data in signed.items(): + pkg = ArchPackage(name=name, + version=data.get('version'), + latest_version=data.get('version'), + description=data.get('description'), + i18n=self.i18n, + installed=True, + repository=repo_map.get(name)) + pkg.categories = self.categories.get(pkg.name) + pkg.downgrade_enabled = True + + if updates: + update_version = updates.get(pkg.name) + + if update_version: + pkg.latest_version = update_version + pkg.update = True + + if disk_loader: + disk_loader.fill(pkg) + + pkgs.append(pkg) def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: - installed = pacman.list_and_map_installed() + self.aur_client.clean_caches() + arch_config = read_config() + installed = pacman.map_installed(repositories=arch_config['repositories'], aur=arch_config['arch']) - apps = [] - if installed and installed['not_signed']: - self.dcache_updater.join() - self.categories_mapper.join() + pkgs = [] + if installed and (installed['not_signed'] or installed['signed']): + map_threads = [] - self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader, internet_available) + if installed['not_signed']: + t = Thread(target=self._fill_aur_pkgs, args=(installed['not_signed'], pkgs, disk_loader, internet_available), daemon=True) + t.start() + map_threads.append(t) - return SearchResult(apps, None, len(apps)) + if installed['signed']: + t = Thread(target=self._fill_repo_pkgs, args=(installed['signed'], pkgs, disk_loader), daemon=True) + t.start() + map_threads.append(t) - def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool: - if not self._check_action_allowed(pkg, watcher): - return False + for t in map_threads: + t.join() - self.local_config = read_config() + return SearchResult(pkgs, None, len(pkgs)) - handler = ProcessHandler(watcher) - app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) - - self._sync_databases(root_password=root_password, handler=handler) - - watcher.change_progress(5) + def _downgrade_aur_pkg(self, context: TransactionContext): + context.build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) try: - if not os.path.exists(app_build_dir): - build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir]))) + if not os.path.exists(context.build_dir): + build_dir = context.handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', context.build_dir]))) if build_dir: - watcher.change_progress(10) - base_name = pkg.get_base_name() - watcher.change_substatus(self.i18n['arch.clone'].format(bold(pkg.name))) - clone = handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(base_name)], cwd=app_build_dir), check_error_output=False)) - watcher.change_progress(30) + context.handler.watcher.change_progress(10) + base_name = context.get_base_name() + context.watcher.change_substatus(self.i18n['arch.clone'].format(bold(context.name))) + clone = context.handler.handle(SystemProcess(subproc=new_subprocess(['git', 'clone', URL_GIT.format(base_name)], + cwd=context.build_dir), check_error_output=False)) + context.watcher.change_progress(30) if clone: - watcher.change_substatus(self.i18n['arch.downgrade.reading_commits']) - clone_path = '{}/{}'.format(app_build_dir, base_name) + context.watcher.change_substatus(self.i18n['arch.downgrade.reading_commits']) + clone_path = '{}/{}'.format(context.build_dir, base_name) + context.project_dir = clone_path srcinfo_path = '{}/.SRCINFO'.format(clone_path) commits = run_cmd("git log", cwd=clone_path) - watcher.change_progress(40) + context.watcher.change_progress(40) if commits: commit_list = re.findall(r'commit (.+)\n', commits) @@ -233,103 +529,327 @@ class ArchManager(SoftwareManager): with open(srcinfo_path) as f: pkgsrc = aur.map_srcinfo(f.read(), srcfields) - if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path), check_error_output=False)): - watcher.print('Could not downgrade anymore. Aborting...') + reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path) + if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): + context.handler.watcher.print('Could not downgrade anymore. Aborting...') return False - if '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == pkg.version: + if '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == context.get_version(): # current version found commit_found = commit elif commit_found: - watcher.change_substatus(self.i18n['arch.downgrade.version_found']) - if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'checkout', commit_found], cwd=clone_path), check_error_output=False)): - watcher.print("Could not rollback to current version's commit") + context.watcher.change_substatus(self.i18n['arch.downgrade.version_found']) + checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_path) + if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)): + context.watcher.print("Could not rollback to current version's commit") return False - if not handler.handle(SystemProcess(subproc=new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_path), check_error_output=False)): - watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found)) + reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_path) + if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): + context.watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found)) return False break - watcher.change_substatus(self.i18n['arch.downgrade.install_older']) - return self._build(pkg.name, base_name, pkg.maintainer, root_password, handler, app_build_dir, clone_path, dependency=False, skip_optdeps=True) + context.watcher.change_substatus(self.i18n['arch.downgrade.install_older']) + return self._build(context) else: - watcher.show_message(title=self.i18n['arch.downgrade.error'], - body=self.i18n['arch.downgrade.impossible'].format(pkg.name), - type_=MessageType.ERROR) + context.watcher.show_message(title=self.i18n['arch.downgrade.error'], + body=self.i18n['arch.downgrade.impossible'].format(context.name), + type_=MessageType.ERROR) return False - watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.downgrade.no_commits'], type_=MessageType.ERROR) + context.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.downgrade.no_commits'], + type_=MessageType.ERROR) return False finally: - if os.path.exists(app_build_dir): - handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', app_build_dir]))) - - self.local_config = None + if os.path.exists(context.build_dir): + context.handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', context.build_dir]))) return False + def _downgrade_repo_pkg(self, context: TransactionContext): + context.watcher.change_substatus(self.i18n['arch.downgrade.searching_stored']) + if not os.path.isdir('/var/cache/pacman/pkg'): + context.watcher.show_message(title=self.i18n['arch.downgrade.error'], + body=self.i18n['arch.downgrade.repo_pkg.no_versions'], + type_=MessageType.ERROR) + return False + + available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(context.name)) + + if not available_files: + context.watcher.show_message(title=self.i18n['arch.downgrade.error'], + body=self.i18n['arch.downgrade.repo_pkg.no_versions'], + type_=MessageType.ERROR) + return False + + reg = re.compile(r'{}-([\w.\-]+)-(x86_64|any|i686).pkg'.format(context.name)) + + versions, version_files = [], {} + for file_path in available_files: + found = reg.findall(os.path.basename(file_path)) + + if found: + ver = found[0][0] + if ver not in versions and ver < context.get_version(): + versions.append(ver) + version_files[ver] = file_path + + context.watcher.change_progress(40) + if not versions: + context.watcher.show_message(title=self.i18n['arch.downgrade.error'], + body=self.i18n['arch.downgrade.repo_pkg.no_versions'], + type_=MessageType.ERROR) + return False + + versions.sort(reverse=True) + + context.watcher.change_progress(50) + + context.install_file = version_files[versions[0]] + if not self._handle_missing_deps(context=context): + return False + + context.watcher.change_substatus(self.i18n['arch.downgrade.install_older']) + context.watcher.change_progress(60) + + return self._install(context) + + def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool: + self.aur_client.clean_caches() + if not self._check_action_allowed(pkg, watcher): + return False + + handler = ProcessHandler(watcher) + + if self._is_database_locked(handler, root_password): + return False + + context = TransactionContext(name=pkg.name, base=pkg.get_base_name(), skip_opt_deps=True, + change_progress=True, dependency=False, repository=pkg.repository, pkg=pkg, + arch_config=read_config(), watcher=watcher, handler=handler, root_password=root_password) + + self._sync_databases(context.config, root_password, handler) + + watcher.change_progress(5) + + if pkg.repository == 'arch': + return self._downgrade_aur_pkg(context) + else: + return self._downgrade_repo_pkg(context) + def clean_cache_for(self, pkg: ArchPackage): if os.path.exists(pkg.get_disk_cache_path()): shutil.rmtree(pkg.get_disk_cache_path()) def _check_action_allowed(self, pkg: ArchPackage, watcher: ProcessWatcher) -> bool: - if user.is_root() and pkg.mirror == 'aur': - watcher.show_message(title=self.i18n['arch.install.aur.root_error.title'], - body=self.i18n['arch.install.aur.root_error.body'], + if user.is_root() and pkg.repository == 'arch': + watcher.show_message(title=self.i18n['arch.install.arch.root_error.title'], + body=self.i18n['arch.install.arch.root_error.body'], type_=MessageType.ERROR) return False return True - def update(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool: - if not self._check_action_allowed(pkg, watcher): + def _is_database_locked(self, handler: ProcessHandler, root_password: str) -> bool: + if os.path.exists('/var/lib/pacman/db.lck'): + handler.watcher.print('pacman database is locked') + msg = '

{}

{}


'.format(self.i18n['arch.action.db_locked.body.l1'], + self.i18n['arch.action.db_locked.body.l2']) + if handler.watcher.request_confirmation(title=self.i18n['arch.action.db_locked.title'].capitalize(), + body=msg, + confirmation_label=self.i18n['arch.action.db_locked.confirmation'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + + try: + if not handler.handle_simple(SimpleProcess(['rm', '-rf', '/var/lib/pacman/db.lck'], root_password=root_password)): + handler.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.action.db_locked.error'], + type_=MessageType.ERROR) + return True + except: + self.logger.error("An error occurred while removing the pacman database lock") + traceback.print_exc() + handler.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.action.db_locked.error'], + type_=MessageType.ERROR) + return True + else: + handler.watcher.print('Action cancelled by the user. Aborting...') + return True + + return False + + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: + self.aur_client.clean_caches() + watcher.change_status("{}...".format(self.i18n['manage_window.status.upgrading'])) + + handler = ProcessHandler(watcher) + + if self._is_database_locked(handler, root_password): + watcher.change_substatus('') return False - self.local_config = read_config() + aur_pkgs, repo_pkgs = [], [] - try: - return self.install(pkg=pkg, root_password=root_password, watcher=watcher, skip_optdeps=True) - finally: - self.local_config = None + for req in (*requirements.to_install, *requirements.to_upgrade): + if req.pkg.repository == 'arch': + aur_pkgs.append(req.pkg) + else: + repo_pkgs.append(req.pkg) - def _uninstall(self, pkg_name: str, root_password: str, handler: ProcessHandler) -> bool: - res = handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', pkg_name, '--noconfirm'], root_password))) + if aur_pkgs and not self._check_action_allowed(aur_pkgs[0], watcher): + return False - if res: - cached_paths = [ArchPackage.disk_cache_path(pkg_name, 'aur'), ArchPackage.disk_cache_path(pkg_name, 'mirror')] + arch_config = read_config() + self._sync_databases(arch_config=arch_config, root_password=root_password, handler=handler) - for path in cached_paths: - if os.path.exists(path): - shutil.rmtree(path) - break + if requirements.to_remove: + to_remove_names = {r.pkg.name for r in requirements.to_remove} + try: + success = handler.handle(pacman.remove_several(to_remove_names, root_password)) + + if not success: + self.logger.error("Could not remove packages: {}".format(', '.join(to_remove_names))) + return False + except: + self.logger.error("An error occured while removing packages: {}".format(', '.join(to_remove_names))) + traceback.print_exc() + return False + + if repo_pkgs: + repo_pkgs_names = [p.name for p in repo_pkgs] + watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_repo_pkgs'])) + self.logger.info("Upgrading {} repository packages: {}".format(len(repo_pkgs_names), + ', '.join(repo_pkgs_names))) + + try: + output_handler = TransactionStatusHandler(watcher, self.i18n, len(repo_pkgs_names), self.logger) + output_handler.start() + success = handler.handle(pacman.upgrade_several(repo_pkgs_names, root_password), output_handler=output_handler.handle) + output_handler.stop_working() + output_handler.join() + + watcher.change_substatus('') + + if success: + watcher.print("Repository packages successfully upgraded") + watcher.change_substatus(self.i18n['arch.upgrade.caching_pkgs_data']) + repo_map = pacman.map_repositories(repo_pkgs_names) + disk.save_several(repo_pkgs_names, repo_map=repo_map, overwrite=True, maintainer=None) + + else: + self.logger.error("An error occurred while upgrading repository packages") + return False + except: + watcher.change_substatus('') + watcher.print("An error occurred while upgrading repository packages") + self.logger.error("An error occurred while upgrading repository packages") + traceback.print_exc() + return False + + watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs'])) + if aur_pkgs: + for pkg in aur_pkgs: + watcher.change_substatus("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], pkg.name, pkg.version)) + context = TransactionContext.gen_context_from(pkg=pkg, arch_config=arch_config, + root_password=root_password, handler=handler) + context.change_progress = False + + try: + if not self.install(pkg=pkg, root_password=root_password, watcher=watcher, context=context): + self.logger.error("Could not upgrade AUR package '{}'".format(pkg.name)) + watcher.change_substatus('') + return False + except: + watcher.change_substatus('') + self.logger.error("An error occurred when upgrading AUR package '{}'".format(pkg.name)) + traceback.print_exc() + return False + + watcher.change_substatus('') + return True + + def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: ProcessHandler) -> bool: + res = handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', *pkgs, '--noconfirm'], root_password), wrong_error_phrase='warning:')) + + installed = pacman.list_installed_names() + + for p in pkgs: + if p not in installed: + cache_path = ArchPackage.disk_cache_path(p) + if os.path.exists(cache_path): + shutil.rmtree(cache_path) return res + def _uninstall(self, context: TransactionContext): + self._update_progress(context, 10) + + required_by = self.deps_analyser.map_all_required_by({context.name}, set()) + + self._update_progress(context, 50) + + to_uninstall = set() + to_uninstall.add(context.name) + + if required_by: + to_uninstall.update(required_by) + + reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in required_by] + reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3) + + msg = '

{}

{}

'.format(self.i18n['arch.uninstall.required_by'].format(bold(context.name), bold(str(len(reqs)))), + self.i18n['arch.uninstall.required_by.advice']) + + if not context.watcher.request_confirmation(title=self.i18n['action.not_allowed'].capitalize(), + body=msg, + components=[reqs_select], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + context.watcher.print("Aborted") + return False + + uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler) + + if uninstalled: + self._update_progress(context, 70) + + if bool(context.config['clean_cached']): # cleaning old versions + context.watcher.change_substatus(self.i18n['arch.uninstall.clean_cached.substatus']) + if os.path.isdir('/var/cache/pacman/pkg'): + for p in to_uninstall: + available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(p)) + + if available_files and not context.handler.handle_simple(SimpleProcess(cmd=['rm', '-rf', *available_files], + root_password=context.root_password)): + context.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.uninstall.clean_cached.error'].format(bold(p)), + type_=MessageType.WARNING) + + self._update_progress(context, 100) + return uninstalled + def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool: - self.local_config = read_config() + self.aur_client.clean_caches() handler = ProcessHandler(watcher) - watcher.change_progress(10) - info = pacman.get_info_dict(pkg.name) - watcher.change_progress(50) - - if info.get('required by'): - pkname = bold(pkg.name) - msg = '{}:

{}

{}'.format(self.i18n['arch.uninstall.required_by'].format(pkname), bold(info['required by']), self.i18n['arch.uninstall.required_by.advice'].format(pkname)) - watcher.show_message(title=self.i18n['error'], body=msg, type_=MessageType.WARNING) + if self._is_database_locked(handler, root_password): return False - uninstalled = self._uninstall(pkg.name, root_password, handler) - watcher.change_progress(100) - self.local_config = None - return uninstalled + return self._uninstall(TransactionContext(name=pkg.name, + change_progress=True, + arch_config=read_config(), + watcher=watcher, + root_password=root_password, + handler=handler)) def get_managed_types(self) -> Set["type"]: return {ArchPackage} - def get_info(self, pkg: ArchPackage) -> dict: + def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict: if pkg.installed: t = Thread(target=self.mapper.fill_package_build, args=(pkg,), daemon=True) t.start() @@ -385,7 +905,20 @@ class ArchManager(SoftwareManager): return info - def get_history(self, pkg: ArchPackage) -> PackageHistory: + def _get_info_repo_pkg(self, pkg: ArchPackage) -> dict: + info = pacman.get_info_dict(pkg.name, remote=not pkg.installed) + if pkg.installed: + info['installed files'] = pacman.list_installed_files(pkg.name) + + return info + + def get_info(self, pkg: ArchPackage) -> dict: + if pkg.repository == 'arch': + return self._get_info_aur_pkg(pkg) + else: + return self._get_info_repo_pkg(pkg) + + def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) try: @@ -421,7 +954,117 @@ class ArchManager(SoftwareManager): if os.path.exists(temp_dir): shutil.rmtree(temp_dir) - def _install_deps(self, deps: List[Tuple[str, str]], root_password: str, handler: ProcessHandler, change_progress: bool = False) -> str: + def _get_history_repo_pkg(self, pkg: ArchPackage) -> PackageHistory: + data = PackageHistory(pkg=pkg, history=[], pkg_status_idx=-1) + + versions = [pkg.latest_version] + version_files = {} # maps the version and tar file + + if pkg.update: + versions.append(pkg.version) + + if os.path.isdir('/var/cache/pacman/pkg'): + available_files = glob.glob("/var/cache/pacman/pkg/{}-*.pkg.tar.*".format(pkg.name)) + + if available_files: + reg = re.compile(r'{}-([\w.\-]+)-(x86_64|any|i686).pkg'.format(pkg.name)) + + for file_path in available_files: + found = reg.findall(os.path.basename(file_path)) + + if found: + ver = found[0][0] + if ver not in versions: + versions.append(ver) + + version_files[ver] = file_path + + versions.sort(reverse=True) + extract_path = '{}/arch/history'.format(TEMP_DIR) + + try: + Path(extract_path).mkdir(parents=True, exist_ok=True) + except: + self.logger.error("Could not create temp dir {} to extract previous versions data".format(extract_path)) + traceback.print_exc() + return data + + try: + for idx, v in enumerate(versions): + cur_version = v.split('-') + cur_data = {'1_version': ''.join(cur_version[0:-1]), + '2_release': cur_version[-1], + '3_date': ''} + + if pkg.version == v: + data.pkg_status_idx = idx + + version_file = version_files.get(v) + + if not version_file: + if v == pkg.version: + cur_data['3_date'] = pacman.get_build_date(pkg.name) + else: + extracted_dir = '{}/{}'.format(extract_path, v) + Path(extracted_dir).mkdir(parents=True, exist_ok=True) + + try: + filext = version_file.split('.')[-1] + run_cmd('tar -C {} -I {} -xvf {} .PKGINFO'.format(extracted_dir, 'zstd' if filext == 'zst' else filext, version_file)) + except tarfile.ReadError: + if v == pkg.version: + cur_data['3_date'] = pacman.get_build_date(pkg.name) + else: + self.logger.error("Could not read file {}. Skipping version {}".format(version_file, v)) + continue + + info_file = '{}/.PKGINFO'.format(extracted_dir) + if os.path.isfile(info_file): + with open(info_file) as f: + for l in f.readlines(): + if l and l.startswith('builddate'): + cur_data['3_date'] = datetime.fromtimestamp(int(l.split('=')[1].strip())) + break + + data.history.append(cur_data) + return data + + finally: + if os.path.exists(extract_path): + try: + self.logger.info("Removing temporary history dir {}".format(extract_path)) + shutil.rmtree(extract_path) + except: + self.logger.error("Could not remove temp path '{}'".format(extract_path)) + raise + + def get_history(self, pkg: ArchPackage) -> PackageHistory: + if pkg.repository == 'arch': + return self._get_history_aur_pkg(pkg) + else: + return self._get_history_repo_pkg(pkg) + + def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: TransactionContext) -> bool: + conflict_msg = '{} {} {}'.format(bold(pkg), self.i18n['and'], bold(conflicting_pkg)) + if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], + body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)): + context.watcher.print(self.i18n['action.cancelled']) + return False + else: + context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflicting_pkg))) + conflict_context = context.clone_base() + conflict_context.change_progress = False + conflict_context.name = conflicting_pkg + + if not self._uninstall(conflict_context): + context.watcher.show_message(title=self.i18n['error'], + body=self.i18n['arch.uninstalling.conflict.fail'].format(bold(conflicting_pkg)), + type_=MessageType.ERROR) + return False + + return True + + def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]]) -> Iterable[str]: """ :param pkgs_repos: :param root_password: @@ -430,34 +1073,71 @@ class ArchManager(SoftwareManager): """ progress_increment = int(100 / len(deps)) progress = 0 - self._update_progress(handler.watcher, 1, change_progress) + self._update_progress(context, 1) + + repo_deps, aur_deps_context = [], [] for dep in deps: - handler.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ({})'.format(dep[0], dep[1])))) - if dep[1] == 'aur': - pkgbase = self.aur_client.get_src_info(dep[0])['pkgbase'] - installed = self._install_from_aur(pkgname=dep[0], pkgbase=pkgbase, maintainer=None, root_password=root_password, handler=handler, dependency=True, change_progress=False) + context.watcher.change_substatus(self.i18n['arch.install.dependency.install'].format(bold('{} ({})'.format(dep[0], dep[1])))) + + if dep[1] == 'arch': + dep_context = context.gen_dep_context(dep[0], dep[1]) + dep_src = self.aur_client.get_src_info(dep[0]) + dep_context.base = dep_src['pkgbase'] + aur_deps_context.append(dep_context) else: - installed = self._install(pkgname=dep[0], maintainer=dep[1], root_password=root_password, handler=handler, install_file=None, mirror=dep[1], change_progress=False) + repo_deps.append(dep[0]) + + if repo_deps: + context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name))) + + all_provided = context.get_provided_map() + + for dep, conflicts in pacman.map_conflicts_with(repo_deps, remote=True).items(): + if conflicts: + for c in conflicts: + source_conflict = all_provided.get(c) + + if source_conflict: + conflict_pkg = [*source_conflict][0] + + if dep != conflict_pkg: + if not self._request_conflict_resolution(dep, conflict_pkg , context): + return {dep} + + status_handler = TransactionStatusHandler(context.watcher, self.i18n, len(repo_deps), self.logger, percentage=len(repo_deps) > 1) + status_handler.start() + installed = context.handler.handle(pacman.install_as_process(pkgpaths=repo_deps, + root_password=context.root_password, + file=False), output_handler=status_handler.handle) + + if installed: + progress += len(repo_deps) * progress_increment + self._update_progress(context, progress) + else: + return repo_deps + + for aur_context in aur_deps_context: + installed = self._install_from_aur(aur_context) if not installed: - return dep[0] + return {aur_context.name} + else: + progress += progress_increment + self._update_progress(context, progress) - progress += progress_increment - self._update_progress(handler.watcher, progress, change_progress) + self._update_progress(context, 100) - self._update_progress(handler.watcher, 100, change_progress) + def _map_repos(self, pkgnames: Iterable[str]) -> dict: + pkg_repos = pacman.get_repositories(pkgnames) # getting repositories set - def _map_repos(self, pkgnames: Set[str]) -> dict: - pkg_mirrors = pacman.get_repositories(pkgnames) # getting mirrors set + if len(pkgnames) != len(pkg_repos): # checking if any dep not found in the distro repos are from AUR + norepos = {p for p in pkgnames if p not in pkg_repos} + for pkginfo in self.aur_client.get_info(norepos): + if pkginfo.get('Name') in norepos: + pkg_repos[pkginfo['Name']] = 'arch' - if len(pkgnames) != len(pkg_mirrors): # checking if any dep not found in the distro mirrors are from AUR - nomirrors = {p for p in pkgnames if p not in pkg_mirrors} - for pkginfo in self.aur_client.get_info(nomirrors): - if pkginfo.get('Name') in nomirrors: - pkg_mirrors[pkginfo['Name']] = 'aur' - - return pkg_mirrors + return pkg_repos def _pre_download_source(self, project_dir: str, watcher: ProcessWatcher) -> bool: if self.context.file_downloader.is_multithreaded(): @@ -472,7 +1152,7 @@ class ArchManager(SoftwareManager): continue else: for f in srcinfo[attr]: - if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f): + if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f): pre_download_files.append(f) if pre_download_files: @@ -491,465 +1171,477 @@ class ArchManager(SoftwareManager): return True - def _should_check_subdeps(self): - return self.local_config['transitive_checking'] + def _build(self, context: TransactionContext) -> bool: + self._pre_download_source(context.project_dir, context.watcher) + self._update_progress(context, 50) - def _build(self, pkgname: str, base_name: str, maintainer: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: - - self._pre_download_source(project_dir, handler.watcher) - - self._update_progress(handler.watcher, 50, change_progress) - - if not self._handle_deps_and_keys(pkgname, root_password, handler, project_dir, check_subdeps=self._should_check_subdeps()): + if not self._handle_aur_package_deps_and_keys(context): return False # building main package - handler.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(pkgname))) - pkgbuilt, output = makepkg.make(project_dir, optimize=self.local_config['optimize'], handler=handler) - self._update_progress(handler.watcher, 65, change_progress) + context.watcher.change_substatus(self.i18n['arch.building.package'].format(bold(context.name))) + optimize = bool(context.config['optimize']) and cpu_manager.supports_performance_mode() and not cpu_manager.all_in_performance() + + cpu_optimized = False + if optimize: + self.logger.info("Setting cpus to performance mode") + cpu_manager.set_mode('performance', context.root_password) + cpu_optimized = True + + try: + pkgbuilt, output = makepkg.make(context.project_dir, optimize=optimize, handler=context.handler) + finally: + if cpu_optimized: + self.logger.info("Setting cpus to powersave mode") + cpu_manager.set_mode('powersave', context.root_password) + + self._update_progress(context, 65) if pkgbuilt: - gen_file = [fname for root, dirs, files in os.walk(build_dir) for fname in files if re.match(r'^{}-.+\.tar\.xz'.format(pkgname), fname)] + gen_file = [fname for root, dirs, files in os.walk(context.build_dir) for fname in files if re.match(r'^{}-.+\.tar\.xz'.format(context.name), fname)] if not gen_file: - handler.watcher.print('Could not find generated .tar.xz file. Aborting...') + context.watcher.print('Could not find generated .tar.xz file. Aborting...') return False - install_file = '{}/{}'.format(project_dir, gen_file[0]) + context.install_file = '{}/{}'.format(context.project_dir, gen_file[0]) - if self._install(pkgname=pkgname, maintainer=maintainer, root_password=root_password, mirror='aur', handler=handler, - install_file=install_file, pkgdir=project_dir, change_progress=change_progress): + if self._install(context=context): - if dependency or skip_optdeps: + if context.dependency or context.skip_opt_deps: return True - handler.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(pkgname))) + context.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(context.name))) - if self._install_optdeps(pkgname, root_password, handler, project_dir): + self._update_progress(context, 100) + if self._install_optdeps(context): return True return False - def _map_known_missing_deps(self, known_deps: Dict[str, str], watcher: ProcessWatcher, check_subdeps: bool = True) -> List[Tuple[str, str]]: - sorted_deps = [] # it will hold the proper order to install the missing dependencies - - repo_deps, aur_deps = set(), set() - - for dep, repo in known_deps.items(): - if repo == 'aur': - aur_deps.add(dep) - else: - repo_deps.add(dep) - - if check_subdeps: - for deps in ((repo_deps, 'repo'), (aur_deps, 'aur')): - if deps[0]: - missing_subdeps = self.deps_analyser.get_missing_subdeps_of(deps[0], deps[1]) - - if missing_subdeps: - for dep in missing_subdeps: - if not dep[1]: - message.show_dep_not_found(dep[0], self.i18n, watcher) - return - - for dep in missing_subdeps: - if dep not in sorted_deps: - sorted_deps.append(dep) - - for dep, repo in known_deps.items(): - if repo != 'aur': - data = (dep, repo) - if data not in sorted_deps: - sorted_deps.append(data) - - for dep in aur_deps: - sorted_deps.append((dep, 'aur')) - - return sorted_deps - - def _check_missing_deps(self, pkgname: str, mirror: str, srcinfo: dict, watcher: ProcessWatcher) -> Dict[str, str]: - if mirror == 'aur': - missing = {} - - missing_subdeps = self.deps_analyser.get_missing_subdeps(name=pkgname, mirror=mirror, srcinfo=srcinfo) - - if missing_subdeps: - for dep in missing_subdeps: - if not dep[1]: - message.show_dep_not_found(dep[0], self.i18n, watcher) - return - - for dep in missing_subdeps: - missing[dep[0]] = dep[1] - - return missing - else: - # TODO - return [] - def _map_unknown_missing_deps(self, deps: List[str], watcher: ProcessWatcher, check_subdeps: bool = True) -> List[Tuple[str, str]]: depnames = {RE_SPLIT_VERSION.split(dep)[0] for dep in deps} dep_repos = self._map_repos(depnames) - if len(depnames) != len(dep_repos): # checking if a dependency could not be found in any mirror + if len(depnames) != len(dep_repos): # checking if a dependency could not be found in any repository for dep in depnames: if dep not in dep_repos: message.show_dep_not_found(dep, self.i18n, watcher) return - return self._map_known_missing_deps(dep_repos, watcher, check_subdeps) + return self.deps_analyser.map_known_missing_deps(dep_repos, watcher, check_subdeps) - def _ask_and_install_missing_deps(self, pkgname: str, root_password: str, missing_deps: List[Tuple[str, str]], handler: ProcessHandler) -> bool: - handler.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(pkgname))) + def _ask_and_install_missing_deps(self, context: TransactionContext, missing_deps: List[Tuple[str, str]]) -> bool: + context.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(context.name))) - if not confirmation.request_install_missing_deps(pkgname, missing_deps, handler.watcher, self.i18n): - handler.watcher.print(self.i18n['action.cancelled']) + if not confirmation.request_install_missing_deps(context.name, missing_deps, context.watcher, self.i18n): + context.watcher.print(self.i18n['action.cancelled']) return False - dep_not_installed = self._install_deps(missing_deps, root_password, handler, change_progress=False) + old_progress_behavior = context.change_progress + context.change_progress = False + deps_not_installed = self._install_deps(context, missing_deps) + context.change_progress = old_progress_behavior - if dep_not_installed: - message.show_dep_not_installed(handler.watcher, pkgname, dep_not_installed, self.i18n) + if deps_not_installed: + message.show_deps_not_installed(context.watcher, context.name, deps_not_installed, self.i18n) return False return True - def _handle_deps_and_keys(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, check_subdeps: bool = True) -> bool: - handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname))) + def _list_missing_deps(self, context: TransactionContext) -> List[Tuple[str, str]]: + context.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(context.name))) + ti = time.time() - if not self.local_config['simple_checking']: - ti = time.time() - with open('{}/.SRCINFO'.format(pkgdir)) as f: + if context.repository == 'arch': + with open('{}/.SRCINFO'.format(context.project_dir)) as f: srcinfo = aur.map_srcinfo(f.read()) - missing_deps = self._check_missing_deps(pkgname=pkgname, mirror='aur', srcinfo=srcinfo, watcher=handler.watcher) - tf = time.time() + pkgs_data = {context.name: self.aur_client.map_update_data(context.name, context.get_version(), srcinfo)} + else: + file = bool(context.install_file) + pkgs_data = pacman.map_updates_data({context.install_file if file else context.name}, files=file) - if missing_deps is None: - self.logger.info("Took {0:.2f} seconds to verify missing dependencies".format(tf - ti)) + deps_data, alread_checked_deps = {}, set() - return False # it means one of the dependencies could not be found - elif missing_deps and check_subdeps: - missing_deps = self._map_known_missing_deps(known_deps=missing_deps, watcher=handler.watcher) - tf = time.time() + missing_deps = self.deps_analyser.map_missing_deps(pkgs_data=pkgs_data, + provided_map=context.get_provided_map(), + aur_index=context.get_aur_idx(self.aur_client), + deps_checked=alread_checked_deps, + deps_data=deps_data, + sort=True, + remote_provided_map=context.get_remote_provided_map(), + remote_repo_map=context.get_remote_repo_map(), + watcher=context.watcher) - if missing_deps is None: - self.logger.info("Took {0:.2f} seconds to verify missing dependencies".format(tf - ti)) - return False # it means one of the dependencies could not be found + tf = time.time() + self.logger.info("Took {0:.2f} seconds to check for missing dependencies".format(tf - ti)) + return missing_deps - self.logger.info("Took {0:.2f} seconds to verify missing dependencies".format(tf - ti)) - if missing_deps: - if not self._ask_and_install_missing_deps(pkgname=pkgname, - root_password=root_password, - missing_deps=missing_deps, - handler=handler): - return False + def _handle_missing_deps(self, context: TransactionContext) -> bool: + missing_deps = self._list_missing_deps(context) - # it is necessary to re-check because missing PGP keys are only notified when there are no missing deps - return self._handle_deps_and_keys(pkgname, root_password, handler, pkgdir, check_subdeps=False) + if missing_deps is None: + return False # called off by the user - ti = time.time() - check_res = makepkg.check(pkgdir, optimize=self.local_config['optimize'], missing_deps=self.local_config['simple_checking'], handler=handler) + if not missing_deps: + return True + elif not self._ask_and_install_missing_deps(context=context, missing_deps=missing_deps): + return False # called off by the user or something went wrong + else: + return True + + def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool: + handled_deps = self._handle_missing_deps(context) + if not handled_deps: + return False + + check_res = makepkg.check(context.project_dir, + optimize=bool(context.config['optimize']), + missing_deps=False, + handler=context.handler) if check_res: - if check_res.get('missing_deps'): - handler.watcher.change_substatus(self.i18n['arch.checking.missing_deps'].format(bold(pkgname))) - missing_deps = self._map_unknown_missing_deps(check_res['missing_deps'], handler.watcher, check_subdeps=check_subdeps) - tf = time.time() - self.logger.info("Took {0:.2f} seconds to verify missing dependencies".format(tf - ti)) - - if missing_deps is None: - return False - - if not self._ask_and_install_missing_deps(pkgname=pkgname, - root_password=root_password, - missing_deps=missing_deps, - handler=handler): - return False - - # it is necessary to re-check because missing PGP keys are only notified when there are no missing deps - return self._handle_deps_and_keys(pkgname, root_password, handler, pkgdir, check_subdeps=False) - if check_res.get('gpg_key'): - if handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.unknown_key.title'], - body=self.i18n['arch.install.aur.unknown_key.body'].format(bold(pkgname), bold(check_res['gpg_key']))): - handler.watcher.change_substatus(self.i18n['arch.aur.install.unknown_key.status'].format(bold(check_res['gpg_key']))) - if not handler.handle(gpg.receive_key(check_res['gpg_key'])): - handler.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.aur.install.unknown_key.receive_error'].format(bold(check_res['gpg_key']))) + if context.watcher.request_confirmation(title=self.i18n['arch.arch.install.unknown_key.title'], + body=self.i18n['arch.install.arch.unknown_key.body'].format(bold(context.name), bold(check_res['gpg_key']))): + context.watcher.change_substatus(self.i18n['arch.arch.install.unknown_key.status'].format(bold(check_res['gpg_key']))) + self.logger.info("Importing GPG key {}".format(check_res['gpg_key'])) + if not context.handler.handle(gpg.receive_key(check_res['gpg_key'])): + self.logger.error("An error occurred while importing the GPG key {}".format(check_res['gpg_key'])) + context.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.arch.install.unknown_key.receive_error'].format(bold(check_res['gpg_key']))) + return False else: - handler.watcher.print(self.i18n['action.cancelled']) + context.watcher.print(self.i18n['action.cancelled']) return False if check_res.get('validity_check'): - body = "

{}

{}

".format(self.i18n['arch.aur.install.validity_check.body'].format(bold(pkgname)), - self.i18n['arch.aur.install.validity_check.proceed']) - return not handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.validity_check.title'].format('( checksum )'), + body = "

{}

{}

".format(self.i18n['arch.arch.install.validity_check.body'].format(bold(context.name)), + self.i18n['arch.arch.install.validity_check.proceed']) + return not context.watcher.request_confirmation(title=self.i18n['arch.arch.install.validity_check.title'].format('( checksum )'), body=body, confirmation_label=self.i18n['no'].capitalize(), deny_label=self.i18n['yes'].capitalize()) return True - def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool: - with open('{}/.SRCINFO'.format(pkgdir)) as f: - odeps = pkgbuild.read_optdeps_as_dict(f.read(), self.context.is_system_x86_64()) + def _install_optdeps(self, context: TransactionContext) -> bool: + odeps = pacman.map_optional_deps({context.name}, remote=False, not_installed=True)[context.name] if not odeps: return True - to_install = {d for d in odeps if not pacman.check_installed(d)} + repo_mapping = self._map_repos(odeps.keys()) - if not to_install: - return True + if repo_mapping: + final_optdeps = {dep: {'desc': odeps.get(dep), 'repository': repo_mapping.get(dep)} for dep, repository in repo_mapping.items() if repo_mapping.get(dep)} - pkg_mirrors = self._map_repos(to_install) - - if pkg_mirrors: - final_optdeps = {dep: {'desc': odeps.get(dep), 'mirror': pkg_mirrors.get(dep)} for dep, mirror in pkg_mirrors.items()} - - deps_to_install = confirmation.request_optional_deps(pkgname, final_optdeps, handler.watcher, self.i18n) + deps_to_install = confirmation.request_optional_deps(context.name, final_optdeps, context.watcher, self.i18n) if not deps_to_install: return True else: - sorted_deps = [] + deps_data = {} + opt_repo_deps, aur_threads = [], [] - if self._should_check_subdeps(): - missing_deps = self._map_known_missing_deps({d: pkg_mirrors[d] for d in deps_to_install}, handler.watcher) + for dep in deps_to_install: + if repo_mapping[dep] == 'arch': + t = Thread(target=self.aur_client.fill_update_data, args=(deps_data, dep, None, None), daemon=True) + t.start() + aur_threads.append(t) + else: + opt_repo_deps.append(dep) - if missing_deps is None: - return True # because the main package installation was successful + if opt_repo_deps: + deps_data.update(pacman.map_updates_data(opt_repo_deps)) - if missing_deps: - same_as_selected = len(deps_to_install) == len(missing_deps) and deps_to_install == {d[0] for d in missing_deps} + for t in aur_threads: + t.join() - if not same_as_selected and not confirmation.request_install_missing_deps(None, missing_deps, handler.watcher, self.i18n): - handler.watcher.print(self.i18n['action.cancelled']) - return True # because the main package installation was successful + provided_map = pacman.map_provided() + remote_provided_map = pacman.map_provided(remote=True) + remote_repo_map = pacman.map_repositories() + aur_index = self.aur_client.read_index() if aur_threads else None + subdeps_data = {} + missing_deps = self.deps_analyser.map_missing_deps(pkgs_data=deps_data, + provided_map=provided_map, + aur_index=aur_index, + deps_checked=set(), + deps_data=subdeps_data, + watcher=context.watcher, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + sort=False) - sorted_deps.extend(missing_deps) - else: - aur_deps, repo_deps = [], [] + if missing_deps is None: + return False # called of by the user - for dep in deps_to_install: - mirror = pkg_mirrors[dep] + to_sort = [] + if missing_deps: + for dep in missing_deps: + # TODO handle multiple providers for a missing dep + if dep[0] not in deps_to_install and dep[1] != '__several__': + to_sort.append(dep[0]) - if mirror == 'aur': - aur_deps.append((dep, mirror)) - else: - repo_deps.append((dep, mirror)) + display_deps_dialog = bool(to_sort) # it means there are subdeps to be installed so a new dialog should be displayed - sorted_deps.extend(repo_deps) - sorted_deps.extend(aur_deps) + to_sort.extend(deps_data.keys()) - dep_not_installed = self._install_deps(sorted_deps, root_password, handler, change_progress=True) + sorted_deps = sorting.sort(to_sort, {**deps_data, **subdeps_data}, provided_map) - if dep_not_installed: - message.show_optdep_not_installed(dep_not_installed, handler.watcher, self.i18n) - return False + if display_deps_dialog and not confirmation.request_install_missing_deps(None, sorted_deps, context.watcher, self.i18n): + context.watcher.print(self.i18n['action.cancelled']) + return True # because the main package installation was successful + + old_progress_behavior = context.change_progress + context.change_progress = True + deps_not_installed = self._install_deps(context, sorted_deps) + context.change_progress = old_progress_behavior + + if deps_not_installed: + message.show_optdeps_not_installed(deps_not_installed, context.watcher, self.i18n) return True - def _install(self, pkgname: str, maintainer: str, root_password: str, mirror: str, handler: ProcessHandler, install_file: str = None, pkgdir: str = '.', change_progress: bool = True): + def _install(self, context: TransactionContext): check_install_output = [] - pkgpath = install_file if install_file else pkgname + pkgpath = context.get_package_path() - handler.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(pkgname))) + context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name))) + self.logger.info("Checking for possible conflicts with '{}'".format(context.name)) - for check_out in SimpleProcess(['pacman', '-U' if install_file else '-S', pkgpath], root_password=root_password, cwd=pkgdir).instance.stdout: + for check_out in SimpleProcess(cmd=['pacman', '-U' if context.install_file else '-S', pkgpath], + root_password=context.root_password, + cwd=context.project_dir or '.').instance.stdout: check_install_output.append(check_out.decode()) - self._update_progress(handler.watcher, 70, change_progress) + self._update_progress(context, 70) + if check_install_output and 'conflict' in check_install_output[-1]: + self.logger.info("Conflicts detected for '{}'".format(context.name)) conflicting_apps = [w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)', check_install_output[-1])] conflict_msg = ' {} '.format(self.i18n['and']).join([bold(c) for c in conflicting_apps]) - if not handler.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], + if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)): - handler.watcher.print(self.i18n['action.cancelled']) + context.watcher.print(self.i18n['action.cancelled']) return False else: # uninstall conflicts - self._update_progress(handler.watcher, 75, change_progress) - to_uninstall = [conflict for conflict in conflicting_apps if conflict != pkgname] + self._update_progress(context, 75) + to_uninstall = [conflict for conflict in conflicting_apps if conflict != context.name] + self.logger.info("Preparing to uninstall conflicting packages: {}".format(to_uninstall)) for conflict in to_uninstall: - handler.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict))) - if not self._uninstall(conflict, root_password, handler): - handler.watcher.show_message(title=self.i18n['error'], + context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict))) + + if not self._uninstall_pkgs(pkgs={conflict}, root_password=context.root_password, handler=context.handler): + context.watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.uninstalling.conflict.fail'].format(bold(conflict)), type_=MessageType.ERROR) return False + else: + self.logger.info("No conflict detected for '{}'".format(context.name)) - handler.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(pkgname))) - self._update_progress(handler.watcher, 80, change_progress) - installed = handler.handle(pacman.install_as_process(pkgpath=pkgpath, root_password=root_password, aur=install_file is not None, pkgdir=pkgdir)) - self._update_progress(handler.watcher, 95, change_progress) + context.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(context.name))) + self._update_progress(context, 80) - if installed and self.context.disk_cache: - handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname))) - if self.context.disk_cache: - disk.save_several({pkgname}, mirror=mirror, maintainer=maintainer, overwrite=True, categories=self.categories) + to_install = [] - self._update_progress(handler.watcher, 100, change_progress) + if context.missing_deps: + to_install.extend((d[0] for d in context.missing_deps)) + + to_install.append(pkgpath) + + if not context.dependency: + status_handler = TransactionStatusHandler(context.watcher, self.i18n, len(to_install), self.logger, percentage=len(to_install) > 1) if not context.dependency else None + status_handler.start() + else: + status_handler = None + + installed = context.handler.handle(process=pacman.install_as_process(pkgpaths=to_install, + root_password=context.root_password, + file=context.has_install_file(), + pkgdir=context.project_dir), + output_handler=status_handler.handle if status_handler else None) + if status_handler: + status_handler.stop_working() + status_handler.join() + + self._update_progress(context, 95) + + if installed: + context.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(context.name))) + pkgnames = {context.name} + repo_map = {context.name: context.repository} + + if context.missing_deps: + for dep in context.missing_deps: + pkgnames.add(dep[0]) + repo_map[dep[0]] = dep[1] + + disk.save_several(pkgnames=pkgnames, + repo_map=repo_map, + maintainer=context.maintainer, + overwrite=True, + categories=self.categories) + + self._update_progress(context, 100) return installed - def _update_progress(self, watcher: ProcessWatcher, val: int, change_progress: bool): - if change_progress: - watcher.change_progress(val) + def _update_progress(self, context: TransactionContext, val: int): + if context.change_progress: + context.watcher.change_progress(val) def _import_pgp_keys(self, pkgname: str, root_password: str, handler: ProcessHandler): srcinfo = self.aur_client.get_src_info(pkgname) if srcinfo.get('validpgpkeys'): - handler.watcher.print(self.i18n['arch.aur.install.verifying_pgp']) + handler.watcher.print(self.i18n['arch.arch.install.verifying_pgp']) keys_to_download = [key for key in srcinfo['validpgpkeys'] if not pacman.verify_pgp_key(key)] if keys_to_download: keys_str = ''.join( ['
- {}'.format(k) for k in keys_to_download]) - msg_body = '{}:
{}

{}'.format(self.i18n['arch.aur.install.pgp.body'].format(bold(pkgname)), + msg_body = '{}:
{}

{}'.format(self.i18n['arch.arch.install.pgp.body'].format(bold(pkgname)), keys_str, self.i18n['ask.continue']) - if handler.watcher.request_confirmation(title=self.i18n['arch.aur.install.pgp.title'], body=msg_body): + if handler.watcher.request_confirmation(title=self.i18n['arch.arch.install.pgp.title'], body=msg_body): for key in keys_to_download: - handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.substatus'].format(bold(key))) + handler.watcher.change_substatus(self.i18n['arch.arch.install.pgp.substatus'].format(bold(key))) if not handler.handle(pacman.receive_key(key, root_password)): handler.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.aur.install.pgp.receive_fail'].format( + body=self.i18n['arch.arch.install.pgp.receive_fail'].format( bold(key)), type_=MessageType.ERROR) return False if not handler.handle(pacman.sign_key(key, root_password)): handler.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.aur.install.pgp.sign_fail'].format( + body=self.i18n['arch.arch.install.pgp.sign_fail'].format( bold(key)), type_=MessageType.ERROR) return False - handler.watcher.change_substatus(self.i18n['arch.aur.install.pgp.success']) + handler.watcher.change_substatus(self.i18n['arch.arch.install.pgp.success']) else: handler.watcher.print(self.i18n['action.cancelled']) return False - def _install_from_aur(self, pkgname: str, pkgbase: str, maintainer: str, root_password: str, handler: ProcessHandler, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: - self._optimize_makepkg(watcher=handler.watcher) + def _install_from_aur(self, context: TransactionContext) -> bool: + self._optimize_makepkg(context.config, context.watcher) - app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) + context.build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) try: - if not os.path.exists(app_build_dir): - build_dir = handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', app_build_dir]))) - self._update_progress(handler.watcher, 10, change_progress) + if not os.path.exists(context.build_dir): + build_dir = context.handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', context.build_dir]))) + self._update_progress(context, 10) if build_dir: - base_name = pkgbase if pkgbase else pkgname + base_name = context.get_base_name() file_url = URL_PKG_DOWNLOAD.format(base_name) file_name = file_url.split('/')[-1] - handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name))) - download = handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=app_build_dir), check_error_output=False)) + context.watcher.change_substatus('{} {}'.format(self.i18n['arch.downloading.package'], bold(file_name))) + download = context.handler.handle(SystemProcess(new_subprocess(['wget', file_url], cwd=context.build_dir), check_error_output=False)) if download: - self._update_progress(handler.watcher, 30, change_progress) - handler.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(base_name))) - uncompress = handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(base_name)], cwd=app_build_dir))) - self._update_progress(handler.watcher, 40, change_progress) + self._update_progress(context, 30) + context.watcher.change_substatus('{} {}'.format(self.i18n['arch.uncompressing.package'], bold(base_name))) + uncompress = context.handler.handle(SystemProcess(new_subprocess(['tar', 'xvzf', '{}.tar.gz'.format(base_name)], cwd=context.build_dir))) + self._update_progress(context, 40) if uncompress: - uncompress_dir = '{}/{}'.format(app_build_dir, base_name) - return self._build(pkgname=pkgname, - base_name=base_name, - maintainer=maintainer, - root_password=root_password, - handler=handler, - build_dir=app_build_dir, - project_dir=uncompress_dir, - dependency=dependency, - skip_optdeps=skip_optdeps, - change_progress=change_progress) + context.project_dir = '{}/{}'.format(context.build_dir, base_name) + + return self._build(context) finally: - if os.path.exists(app_build_dir): - handler.handle(SystemProcess(new_subprocess(['rm', '-rf', app_build_dir]))) + if os.path.exists(context.build_dir): + context.handler.handle(SystemProcess(new_subprocess(['rm', '-rf', context.build_dir]))) return False - def _sync_databases(self, root_password: str, handler: ProcessHandler): - sync_path = '{}/arch/sync'.format(TEMP_DIR) + def _sync_databases(self, arch_config: dict, root_password: str, handler: ProcessHandler, change_substatus: bool = True): + if bool(arch_config['sync_databases']) and database.should_sync(arch_config, handler, self.logger): + if change_substatus: + handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus']) - if self.local_config['sync_databases']: - if os.path.exists(sync_path): - with open(sync_path) as f: - sync_file = f.read() - - try: - sync_time = datetime.fromtimestamp(int(sync_file)) - now = datetime.now() - - if (now - sync_time).days > 0: - self.logger.info("Package databases synchronization out of date") - else: - msg = "Package databases already synchronized" - self.logger.info(msg) - handler.watcher.print(msg) - return - except: - self.logger.warning("Could not convert the database synchronization time from '{}".format(sync_path)) - traceback.print_exc() - - handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus']) - synced, output = handler.handle_simple(pacman.sync_databases(root_password=root_password, - force=True)) + synced, output = handler.handle_simple(pacman.sync_databases(root_password=root_password, force=True)) if synced: - try: - Path('/'.join(sync_path.split('/')[0:-1])).mkdir(parents=True, exist_ok=True) - with open(sync_path, 'w+') as f: - f.write(str(int(time.time()))) - except: - traceback.print_exc() + database.register_sync(self.logger) else: self.logger.warning("It was not possible to synchronized the package databases") handler.watcher.change_substatus(self.i18n['arch.sync_databases.substatus.error']) - else: - msg = "Package databases synchronization disabled" - handler.watcher.print(msg) - self.logger.info(msg) - def _optimize_makepkg(self, watcher: ProcessWatcher): - if self.local_config['optimize'] and not os.path.exists(CUSTOM_MAKEPKG_FILE): + def _optimize_makepkg(self, arch_config: dict, watcher: ProcessWatcher): + if arch_config['optimize'] and not os.path.exists(CUSTOM_MAKEPKG_FILE): watcher.change_substatus(self.i18n['arch.makepkg.optimizing']) - ArchCompilationOptimizer(self.context.logger).optimize() + ArchCompilationOptimizer(arch_config, self.i18n, self.context.logger).optimize() + + def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, context: TransactionContext = None) -> bool: + self.aur_client.clean_caches() - def install(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher, skip_optdeps: bool = False) -> bool: if not self._check_action_allowed(pkg, watcher): return False - clean_config = False + handler = ProcessHandler(watcher) if not context else context.handler - if not self.local_config: - self.local_config = read_config() - clean_config = True + if self._is_database_locked(handler, root_password): + return False - handler = ProcessHandler(watcher) - - self._sync_databases(root_password=root_password, handler=handler) - - if pkg.mirror == 'aur': - res = self._install_from_aur(pkg.name, pkg.package_base, pkg.maintainer, root_password, handler, dependency=False, skip_optdeps=skip_optdeps) + if context: + install_context = context else: - res = self._install(pkgname=pkg.name, maintainer=pkg.mirror, root_password=root_password, handler=handler, install_file=None, mirror=pkg.mirror, change_progress=False) + install_context = TransactionContext.gen_context_from(pkg=pkg, handler=handler, arch_config=read_config(), + root_password=root_password) + install_context.skip_opt_deps = False - if res: - if os.path.exists(pkg.get_disk_data_path()): - with open(pkg.get_disk_data_path()) as f: - data = f.read() - if data: - data = json.loads(data) - pkg.fill_cached_data(data) + self._sync_databases(arch_config=install_context.config, root_password=root_password, handler=handler) - if clean_config: - self.local_config = None + if pkg.repository == 'arch': + res = self._install_from_aur(install_context) + else: + res = self._install_from_repository(install_context) + + if res and os.path.exists(pkg.get_disk_data_path()): + with open(pkg.get_disk_data_path()) as f: + data = f.read() + if data: + data = json.loads(data) + pkg.fill_cached_data(data) + + return res + + def _install_from_repository(self, context: TransactionContext) -> bool: + try: + missing_deps = self._list_missing_deps(context) + except PackageNotFoundException: + self.logger.error("Package '{}' was not found") + return False + + if missing_deps is None: + return False # called off by the user + + if missing_deps: + if any((dep for dep in missing_deps if dep[1] == 'arch')): + context.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.install.repo_pkg.error.aur_deps'], + type_=MessageType.ERROR) + return False + + context.missing_deps = missing_deps + context.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(context.name))) + + if not confirmation.request_install_missing_deps(context.name, missing_deps, context.watcher, self.i18n): + context.watcher.print(self.i18n['action.cancelled']) + return False + + res = self._install(context) + + if res and not context.skip_opt_deps: + self._update_progress(context, 100) + return self._install_optdeps(context) return res @@ -965,7 +1657,7 @@ class ArchManager(SoftwareManager): def can_work(self) -> bool: try: - return self.arch_distro and pacman.is_enabled() and self._is_wget_available() + return self.arch_distro and pacman.is_available() and self._is_wget_available() except FileNotFoundError: return False @@ -980,23 +1672,65 @@ class ArchManager(SoftwareManager): pass def requires_root(self, action: str, pkg: ArchPackage): + if action == 'prepare': + arch_config = read_config() + + if arch_config['refresh_mirrors_startup'] and mirrors.should_sync(self.logger): + return True + + return arch_config['sync_databases_startup'] and database.should_sync(arch_config, None, self.logger) + return action != 'search' - def prepare(self): - self.dcache_updater.start() - ArchCompilationOptimizer(self.context.logger).start() - self.categories_mapper.start() - AURIndexUpdater(self.context).start() + def _start_category_task(self, task_man: TaskManager): + task_man.register_task('arch_aur_cats', self.i18n['task.download_categories'].format('Arch'), get_icon_path()) + task_man.update_progress('arch_aur_cats', 50, None) + + def _finish_category_task(self, task_man: TaskManager): + task_man.update_progress('arch_aur_cats', 100, None) + task_man.finish_task('arch_aur_cats') + + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): + arch_config = read_config(update_file=True) + + if arch_config['arch'] or arch_config['repositories']: + ArchDiskCacheUpdater(task_manager, arch_config, self.i18n, self.context.logger).start() + + if arch_config['arch']: + ArchCompilationOptimizer(arch_config, self.i18n, self.context.logger, task_manager).start() + + CategoriesDownloader(id_='Arch', http_client=self.context.http_client, logger=self.context.logger, + manager=self, url_categories_file=URL_CATEGORIES_FILE, disk_cache_dir=ARCH_CACHE_PATH, + categories_path=CATEGORIES_FILE_PATH, + before=lambda: self._start_category_task(task_manager), + after=lambda: self._finish_category_task(task_manager)).start() + + if arch_config['arch'] and internet_available: + self.index_aur = AURIndexUpdater(self.context) + self.index_aur.start() + + refresh_mirrors = None + if internet_available and arch_config['repositories'] and arch_config['refresh_mirrors_startup'] \ + and pacman.is_mirrors_available() and mirrors.should_sync(self.logger): + + refresh_mirrors = RefreshMirrors(taskman=task_manager, i18n=self.i18n, + root_password=root_password, logger=self.logger, + sort_limit=arch_config['mirrors_sort_limit']) + refresh_mirrors.start() + + if internet_available and (refresh_mirrors or (arch_config['sync_databases_startup'] and database.should_sync(arch_config, None, self.logger))): + SyncDatabases(taskman=task_manager, root_password=root_password, i18n=self.i18n, + logger=self.logger, refresh_mirrors=refresh_mirrors).start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed - return [PackageUpdate(p.id, p.latest_version, 'aur') for p in installed if p.update] + return [PackageUpdate(p.name, p.latest_version, p.get_update_type(), p.name) for p in installed if p.update] def list_warnings(self, internet_available: bool) -> List[str]: warnings = [] if self.arch_distro: - if not pacman.is_enabled(): + if not pacman.is_available(): warnings.append(self.i18n['arch.warning.disabled'].format(bold('pacman'))) if not self._is_wget_available(): @@ -1030,7 +1764,6 @@ class ArchManager(SoftwareManager): if api_res: res = [] - self.categories_mapper.join() for pkg in api_res: if pkg.get('Name') in suggestions: res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories), suggestions[pkg['Name']])) @@ -1048,56 +1781,87 @@ class ArchManager(SoftwareManager): def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass - def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool, max_width: int) -> SingleSelectComponent: + def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool, max_width: int, capitalize_label: bool = True) -> SingleSelectComponent: opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), InputOption(label=self.i18n['no'].capitalize(), value=False)] - return SingleSelectComponent(label=self.i18n[label_key].capitalize(), + return SingleSelectComponent(label=self.i18n[label_key], options=opts, default_option=[o for o in opts if o.value == value][0], max_per_line=len(opts), type_=SelectViewType.RADIO, tooltip=self.i18n[tooltip_key], max_width=max_width, - id_=id_) + id_=id_, + capitalize_label=capitalize_label) def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: - config = read_config() + local_config = read_config() max_width = floor(screen_width * 0.15) + db_sync_start = self._gen_bool_selector(id_='sync_dbs_start', + label_key='arch.config.sync_dbs', + tooltip_key='arch.config.sync_dbs_start.tip', + value=bool(local_config['sync_databases_startup']), + max_width=max_width) + + db_sync_start.label += ' ( {} )'.format(self.i18n['initialization'].capitalize()) + fields = [ + self._gen_bool_selector(id_='repos', + label_key='arch.config.repos', + tooltip_key='arch.config.repos.tip', + value=bool(local_config['repositories']), + max_width=max_width), + self._gen_bool_selector(id_='arch', + label_key='arch.config.arch', + tooltip_key='arch.config.arch.tip', + value=bool(local_config['arch']), + max_width=max_width, + capitalize_label=False), self._gen_bool_selector(id_='opts', label_key='arch.config.optimize', tooltip_key='arch.config.optimize.tip', - value=bool(config['optimize']), - max_width=max_width), - self._gen_bool_selector(id_='simple_dep_check', - label_key='arch.config.simple_dep_check', - tooltip_key='arch.config.simple_dep_check.tip', - value=bool(config['simple_checking']), - max_width=max_width), - self._gen_bool_selector(id_='trans_dep_check', - label_key='arch.config.trans_dep_check', - tooltip_key='arch.config.trans_dep_check.tip', - value=bool(config['transitive_checking']), + value=bool(local_config['optimize']), max_width=max_width), self._gen_bool_selector(id_='sync_dbs', label_key='arch.config.sync_dbs', tooltip_key='arch.config.sync_dbs.tip', - value=bool(config['sync_databases']), - max_width=max_width) + value=bool(local_config['sync_databases']), + max_width=max_width), + db_sync_start, + self._gen_bool_selector(id_='clean_cached', + label_key='arch.config.clean_cache', + tooltip_key='arch.config.clean_cache.tip', + value=bool(local_config['clean_cached']), + max_width=max_width), + self._gen_bool_selector(id_='ref_mirs', + label_key='arch.config.refresh_mirrors', + tooltip_key='arch.config.refresh_mirrors.tip', + value=bool(local_config['refresh_mirrors_startup']), + max_width=max_width), + TextInputComponent(id_='mirrors_sort_limit', + label=self.i18n['arch.config.mirrors_sort_limit'], + tooltip=self.i18n['arch.config.mirrors_sort_limit.tip'], + only_int=True, + max_width=max_width, + value=local_config['mirrors_sort_limit'] if isinstance(local_config['mirrors_sort_limit'], int) else '') ] - return PanelComponent([FormComponent(fields, label=self.i18n['installation'].capitalize())]) + return PanelComponent([FormComponent(fields, spaces=False)]) def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config = read_config() form_install = component.components[0] + config['repositories'] = form_install.get_component('repos').get_selected() + config['arch'] = form_install.get_component('arch').get_selected() config['optimize'] = form_install.get_component('opts').get_selected() - config['transitive_checking'] = form_install.get_component('trans_dep_check').get_selected() config['sync_databases'] = form_install.get_component('sync_dbs').get_selected() - config['simple_checking'] = form_install.get_component('simple_dep_check').get_selected() + config['sync_databases_startup'] = form_install.get_component('sync_dbs_start').get_selected() + config['clean_cached'] = form_install.get_component('clean_cached').get_selected() + config['refresh_mirrors_startup'] = form_install.get_component('ref_mirs').get_selected() + config['mirrors_sort_limit'] = form_install.get_component('mirrors_sort_limit').get_int_value() try: save_config(config, CONFIG_FILE) @@ -1105,133 +1869,170 @@ class ArchManager(SoftwareManager): except: return False, [traceback.format_exc()] - def sort_update_order(self, pkgs: List[ArchPackage]) -> List[ArchPackage]: - pkg_deps = {} # maps the package instance and a set with all its dependencies - names_map = {} # maps all the package provided names to the package instance + def get_upgrade_requirements(self, pkgs: List[ArchPackage], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements: + self.aur_client.clean_caches() + arch_config = read_config() + self._sync_databases(arch_config=arch_config, root_password=root_password, handler=ProcessHandler(watcher), change_substatus=False) + self.aur_client.clean_caches() + try: + return UpdatesSummarizer(self.aur_client, self.i18n, self.logger, self.deps_analyser, watcher).summarize(pkgs, root_password, arch_config) + except PackageNotFoundException: + pass # when nothing is returned, the upgrade is called off by the UI - def _add_info(pkg: ArchPackage): - try: - srcinfo = self.aur_client.get_src_info(pkg.name) + def get_custom_actions(self) -> List[CustomSoftwareAction]: + actions = [] - names_map[pkg.name] = pkg - names = srcinfo.get('pkgname') + arch_config = read_config() - if isinstance(names, list): - for n in names: - names_map[n] = pkg + if pacman.is_mirrors_available(): + actions.append(self.custom_actions['ref_mirrors']) - pkg_deps[pkg] = self.aur_client.extract_required_dependencies(srcinfo) - except: - pkg_deps[pkg] = None - self.logger.warning("Could not retrieve dependencies for '{}'".format(pkg.name)) - traceback.print_exc() + actions.append(self.custom_actions['ref_dbs']) + actions.append(self.custom_actions['clean_cache']) - threads = [] - for pkg in pkgs: - t = Thread(target=_add_info, args=(pkg, ), daemon=True) - t.start() - threads.append(t) + if bool(arch_config['repositories']): + actions.append(self.custom_actions['sys_up']) - for t in threads: - t.join() + return actions - return self._sort_deps(pkg_deps, names_map) + def fill_sizes(self, pkgs: List[ArchPackage]): + installed, new, all_names, installed_names = [], [], [], [] - @classmethod - def _sort_deps(cls, pkg_deps: Dict[ArchPackage, Set[str]], names_map: Dict[str, ArchPackage]) -> List[ArchPackage]: - sorted_names, not_sorted = {}, {} - pkg_map = {} + for p in pkgs: + if p.repository != 'arch': + all_names.append(p.name) + if p.installed: + installed.append(p) + installed_names.append(p.name) + else: + new.append(p) - # first adding all with no deps: - for pkg, deps in pkg_deps.items(): - if not deps: - sorted_names[pkg.name] = len(sorted_names) + new_sizes = pacman.get_update_size(all_names) + + if new_sizes: + if new: + for p in new: + p.size = new_sizes.get(p.name) + + if installed: + installed_sizes = pacman.get_installed_size(installed_names) + + for p in installed: + p.size = installed_sizes.get(p.name) + new_size = new_sizes.get(p.name) + + if p.size is None: + p.size = new_size + elif new_size is not None: + p.size = new_size - p.size + + def upgrade_system(self, root_password: str, watcher: ProcessWatcher) -> bool: + installed = pacman.map_installed(repositories=True, aur=False) + + if not installed or not installed['signed']: + watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'], + body=self.i18n['arch.custom_action.upgrade_system.no_updates'], + type_=MessageType.INFO) + return False + + pkgs = [] + self._fill_repo_pkgs(installed['signed'], pkgs, None) + + to_update = [p for p in pkgs if p.update] + + if not to_update: + watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'], + body=self.i18n['arch.custom_action.upgrade_system.no_updates'], + type_=MessageType.INFO) + return False + + # icon_path = get_repo_icon_path() + + # pkg_opts, size = [], 0 + + # self.fill_sizes(to_update) + # + # for pkg in to_update: + # lb = '{} ( {} > {} ) - {}: {}'.format(pkg.name, + # pkg.version, + # pkg.latest_version, + # self.i18n['size'].capitalize(), + # '?' if pkg.size is None else get_human_size_str(pkg.size)) + # pkg_opts.append(InputOption(label=lb, + # value=pkg.name, + # read_only=True, + # icon_path=icon_path)) + # + # if pkg.size is not None: + # size += pkg.size + # + # pkg_opts.sort(key=lambda o: o.label) + + # select = MultipleSelectComponent(label='', + # options=pkg_opts, + # default_options=set(pkg_opts)) + + # if watcher.request_confirmation(title=self.i18n['arch.custom_action.upgrade_system'], + # body="{}. {}: {}".format(self.i18n['arch.custom_action.upgrade_system.pkgs'], + # self.i18n['size'].capitalize(), + # get_human_size_str(size)), + # confirmation_label=self.i18n['proceed'].capitalize(), + # deny_label=self.i18n['cancel'].capitalize(), + # components=[select]): + + # watcher.change_substatus(self.i18n['arch.custom_action.upgrade_system.substatus']) + handler = ProcessHandler(watcher) + + if self._is_database_locked(handler, root_password): + return False + + success, output = handler.handle_simple(pacman.upgrade_system(root_password)) + + if not success or 'error:' in output: + watcher.show_message(title=self.i18n['arch.custom_action.upgrade_system'], + body="An error occurred during the upgrade process. Check out the {}".format( + bold('Details')), + type_=MessageType.ERROR) + return False + else: + database.register_sync(self.logger) + msg = '

{}


{}

{}

'.format(self.i18n['action.update.success.reboot.line1'], + self.i18n['action.update.success.reboot.line2'], + self.i18n['action.update.success.reboot.line3']) + watcher.request_reboot(msg) + return True + + def clean_cache(self, root_password: str, watcher: ProcessWatcher) -> bool: + + cache_dir = pacman.get_cache_dir() + + if not cache_dir or not os.path.isdir(cache_dir): + watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(), + body=self.i18n['arch.custom_action.clean_cache.no_dir'.format(bold(cache_dir))].capitalize(), + type_=MessageType.WARNING) + return False + + text = '

{}.

{}.

{}.

'.format(self.i18n['arch.custom_action.clean_cache.msg1'], + self.i18n['arch.custom_action.clean_cache.msg2'], + self.i18n['arch.custom_action.clean_cache.msg3']) + + if watcher.request_confirmation(title=self.i18n['arch.custom_action.clean_cache'].capitalize(), + body=text, + confirmation_label=self.i18n['clean'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + rm = SimpleProcess(cmd=['rm', '-rf', '{}/*'.format(cache_dir)], root_password=root_password) + + handler = ProcessHandler(watcher) + success, output = handler.handle_simple(rm) + + if success: + watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(), + body=self.i18n['arch.custom_action.clean_cache.success'], + type_=MessageType.INFO) + return True else: - not_sorted[pkg.name] = pkg + watcher.show_message(title=self.i18n['arch.custom_action.clean_cache'].capitalize(), + body=self.i18n['arch.custom_action.clean_cache.fail'], + type_=MessageType.ERROR) - pkg_map[pkg.name] = pkg - - # now adding all that depends on another: - for name, pkg in not_sorted.items(): - cls._add_to_sort(pkg, pkg_deps, sorted_names, not_sorted, names_map) - - position_map = {'{}-{}'.format(i, n): pkg_map[n] for n, i in sorted_names.items()} - return [position_map[idx] for idx in sorted(position_map)] - - @classmethod - def _add_to_sort(cls, pkg: ArchPackage, pkg_deps: Dict[ArchPackage, Set[str]], sorted_names: Dict[str, int], not_sorted: Dict[str, ArchPackage], names_map: Dict[str, ArchPackage]) -> int: - idx = sorted_names.get(pkg.name) - - if idx is not None: - return idx - else: - idx = len(sorted_names) - sorted_names[pkg.name] = idx - - for dep in pkg_deps[pkg]: - dep_idx = sorted_names.get(dep) - - if dep_idx is not None: - idx = dep_idx + 1 - else: - dep_pkg = names_map.get(dep) - - if dep_pkg: # it means the declared dep is mapped differently from the provided packages to update - dep_idx = sorted_names.get(dep_pkg.name) - - if dep_idx is not None: - idx = dep_idx + 1 - else: - dep_idx = cls._add_to_sort(dep_pkg, pkg_deps, sorted_names, not_sorted, names_map) - idx = dep_idx + 1 - - elif dep in not_sorted: # it means the dep is one of the packages to sort, but it not sorted yet - dep_idx = cls._add_to_sort(not_sorted[dep], pkg_deps, sorted_names, not_sorted, names_map) - idx = dep_idx + 1 - - sorted_names[pkg.name] = idx - - return sorted_names[pkg.name] - - def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict): - version = None - - if pkg_data[1] == 'aur': - try: - info = self.aur_client.get_src_info(pkg_data[0]) - - if info: - version = info.get('pkgver') - - if not version: - self.logger.warning("No version declared in SRCINFO of '{}'".format(pkg_data[0])) - else: - self.logger.warning("Could not retrieve the SRCINFO for '{}'".format(pkg_data[0])) - except: - self.logger.warning("Could not retrieve the SRCINFO for '{}'".format(pkg_data[0])) - else: - version = pacman.get_version_for_not_installed(pkg_data[0]) - - output[idx] = ArchPackage(name=pkg_data[0], version=version, latest_version=version, mirror=pkg_data[1], i18n=self.i18n) - - def get_update_requirements(self, pkgs: List[ArchPackage], watcher: ProcessWatcher) -> List[ArchPackage]: - deps = self._map_known_missing_deps({p.get_base_name(): 'aur' for p in pkgs}, watcher) - - if deps: # filtering selected packages - selected_names = {p.name for p in pkgs} - deps = [dep for dep in deps if dep[0] not in selected_names] - - if deps: - map_threads, sorted_pkgs = [], {} - - for idx, dep in enumerate(deps): - t = Thread(target=self._map_and_add_package, args=(dep, idx, sorted_pkgs), daemon=True) - t.start() - map_threads.append(t) - - for t in map_threads: - t.join() - - return [sorted_pkgs[idx] for idx in sorted(sorted_pkgs)] - else: - return [] + return False diff --git a/bauh/gems/arch/cpu_manager.py b/bauh/gems/arch/cpu_manager.py new file mode 100644 index 00000000..421e1526 --- /dev/null +++ b/bauh/gems/arch/cpu_manager.py @@ -0,0 +1,38 @@ +import multiprocessing +import os +import traceback + +from bauh.commons.system import new_root_subprocess + + +def supports_performance_mode(): + return os.path.exists('/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor') + + +def all_in_performance() -> bool: + for i in range(multiprocessing.cpu_count()): + with open('/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor'.format(i)) as f: + if f.read().strip() != 'performance': + return False + + return False + + +def set_mode(mode: str, root_password: str): + new_gov_file = '/tmp/bauh_scaling_governor' + with open(new_gov_file, 'w+') as f: + f.write(mode) + + for i in range(multiprocessing.cpu_count()): + try: + gov_file = '/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor'.format(i) + replace = new_root_subprocess(['cp', new_gov_file, gov_file], root_password=root_password) + replace.wait() + except: + traceback.print_exc() + + if os.path.exists(new_gov_file): + try: + os.remove(new_gov_file) + except: + traceback.print_exc() diff --git a/bauh/gems/arch/database.py b/bauh/gems/arch/database.py new file mode 100644 index 00000000..e738c0e1 --- /dev/null +++ b/bauh/gems/arch/database.py @@ -0,0 +1,52 @@ +import logging +import os +import time +import traceback +from datetime import datetime +from logging import Logger +from pathlib import Path + +from bauh.api.constants import CACHE_PATH +from bauh.commons.system import ProcessHandler + +SYNC_FILE = '{}/arch/db_sync'.format(CACHE_PATH) + + +def should_sync(arch_config: dict, handler: ProcessHandler, logger: logging.Logger): + if arch_config['arch'] or arch_config['repositories']: + if os.path.exists(SYNC_FILE): + with open(SYNC_FILE) as f: + sync_file = f.read() + + try: + sync_time = datetime.fromtimestamp(int(sync_file)) + now = datetime.now() + + if now > sync_time and now.day != sync_time.day: + logger.info("Package databases synchronization out of date") + else: + msg = "Package databases already synchronized" + logger.info(msg) + if handler: + handler.watcher.print(msg) + return False + except: + logger.warning("Could not convert the database synchronization time from '{}".format(SYNC_FILE)) + traceback.print_exc() + return True + else: + msg = "Package databases synchronization disabled" + if handler: + handler.watcher.print(msg) + logger.info(msg) + return False + + +def register_sync(logger: Logger): + try: + Path('/'.join(SYNC_FILE.split('/')[0:-1])).mkdir(parents=True, exist_ok=True) + with open(SYNC_FILE, 'w+') as f: + f.write(str(int(time.time()))) + except: + logger.error("Could not write to database sync file '{}'".format(SYNC_FILE)) + traceback.print_exc() diff --git a/bauh/gems/arch/depedencies.py b/bauh/gems/arch/depedencies.py deleted file mode 100644 index 3e248c23..00000000 --- a/bauh/gems/arch/depedencies.py +++ /dev/null @@ -1,130 +0,0 @@ -from threading import Thread -from typing import Set, List, Tuple - -from bauh.gems.arch import pacman -from bauh.gems.arch.aur import AURClient - - -class DependenciesAnalyser: - - def __init__(self, aur_client: AURClient): - self.aur_client = aur_client - - def _fill_mirror(self, name: str, output: List[Tuple[str, str]]): - - mirror = pacman.read_repository_from_info(name) - - if mirror: - output.append((name, mirror)) - return - - guess = pacman.guess_repository(name) - - if guess: - output.append(guess) - return - - aur_info = self.aur_client.get_src_info(name) - - if aur_info: - output.append((name, 'aur')) - return - - output.append((name, '')) - - def get_missing_packages(self, names: Set[str], mirror: str = None, in_analysis: Set[str] = None) -> List[Tuple[str, str]]: - """ - :param names: - :param mirror: - :param in_analysis: global set storing all names in analysis to avoid repeated recursion - :return: - """ - global_in_analysis = in_analysis if in_analysis else set() - - missing_names = pacman.check_missing({n for n in names if n not in in_analysis}) - - if missing_names: - missing_root = [] - threads = [] - - if not mirror: - for name in missing_names: - t = Thread(target=self._fill_mirror, args=(name, missing_root)) - t.start() - threads.append(t) - - for t in threads: - t.join() - - threads.clear() - - # checking if there is any unknown dependency: - for rdep in missing_root: - if not rdep[1]: - return missing_root - else: - global_in_analysis.add(rdep[0]) - else: - for missing in missing_names: - missing_root.append((missing, mirror)) - global_in_analysis.add(missing) - - missing_sub = [] - for rdep in missing_root: - subdeps = self.aur_client.get_required_dependencies(rdep[0]) if rdep[1] == 'aur' else pacman.read_dependencies(rdep[0]) - subdeps_not_analysis = {sd for sd in subdeps if sd not in global_in_analysis} - - if subdeps_not_analysis: - missing_subdeps = self.get_missing_packages(subdeps_not_analysis, in_analysis=global_in_analysis) - - # checking if there is any unknown: - if missing_subdeps: - for subdep in missing_subdeps: - if not subdep[0]: - return [*missing_subdeps, *missing_root] - - if subdep[0] not in missing_names: - missing_sub.append(subdep) - return [*missing_sub, *missing_root] - - def get_missing_subdeps_of(self, names: Set[str], mirror: str) -> List[Tuple[str, str]]: - missing = [] - already_added = {*names} - in_analyses = {*names} - - for name in names: - subdeps = self.aur_client.get_required_dependencies(name) if mirror == 'aur' else pacman.read_dependencies(name) - - if subdeps: - missing_subdeps = self.get_missing_packages(subdeps, in_analysis=in_analyses) - - if missing_subdeps: - for subdep in missing_subdeps: # checking if there is any unknown: - if subdep[0] not in already_added: - missing.append(subdep) - - if not subdep[0]: - return missing - return missing - - def get_missing_subdeps(self, name: str, mirror: str, srcinfo: dict = None) -> List[Tuple[str, str]]: - missing = [] - already_added = {name} - in_analyses = {name} - - if mirror == 'aur': - subdeps = self.aur_client.get_required_dependencies(name) if not srcinfo else self.aur_client.extract_required_dependencies(srcinfo) - else: - subdeps = pacman.read_dependencies(name) - - if subdeps: - missing_subdeps = self.get_missing_packages(subdeps, in_analysis=in_analyses) - - if missing_subdeps: - for subdep in missing_subdeps: # checking if there is any unknown: - if subdep[0] not in already_added: - missing.append(subdep) - - if not subdep[0]: - return missing - return missing diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py new file mode 100644 index 00000000..d511ecb5 --- /dev/null +++ b/bauh/gems/arch/dependencies.py @@ -0,0 +1,466 @@ +import re +from distutils.version import LooseVersion +from threading import Thread +from typing import Set, List, Tuple, Dict, Iterable + +from bauh.api.abstract.handler import ProcessWatcher +from bauh.gems.arch import pacman, message, sorting, confirmation +from bauh.gems.arch.aur import AURClient +from bauh.gems.arch.exceptions import PackageNotFoundException +from bauh.view.util.translation import I18n + + +class DependenciesAnalyser: + + def __init__(self, aur_client: AURClient, i18n: I18n): + self.aur_client = aur_client + self.i18n = i18n + self.re_dep_operator = re.compile(r'([<>=]+)') + + def _fill_repository(self, name: str, output: List[Tuple[str, str]]): + + repository = pacman.read_repository_from_info(name) + + if repository: + output.append((name, repository)) + return + + guess = pacman.guess_repository(name) + + if guess: + output.append(guess) + return + + aur_info = self.aur_client.get_src_info(name) + + if aur_info: + output.append((name, 'arch')) + return + + output.append((name, '')) + + def get_missing_packages(self, names: Set[str], repository: str = None, in_analysis: Set[str] = None) -> List[ + Tuple[str, str]]: + """ + :param names: + :param repository: + :param in_analysis: global set storing all names in analysis to avoid repeated recursion + :return: + """ + global_in_analysis = in_analysis if in_analysis else set() + + missing_names = pacman.check_missing({n for n in names if n not in in_analysis}) + + if missing_names: + missing_root = [] + threads = [] + + if not repository: + for name in missing_names: + t = Thread(target=self._fill_repository, args=(name, missing_root)) + t.start() + threads.append(t) + + for t in threads: + t.join() + + threads.clear() + + # checking if there is any unknown dependency: + for rdep in missing_root: + if not rdep[1]: + return missing_root + else: + global_in_analysis.add(rdep[0]) + else: + for missing in missing_names: + missing_root.append((missing, repository)) + global_in_analysis.add(missing) + + missing_sub = [] + for rdep in missing_root: + subdeps = self.aur_client.get_required_dependencies(rdep[0]) if rdep[ + 1] == 'arch' else pacman.read_dependencies( + rdep[0]) + subdeps_not_analysis = {sd for sd in subdeps if sd not in global_in_analysis} + + if subdeps_not_analysis: + missing_subdeps = self.get_missing_packages(subdeps_not_analysis, in_analysis=global_in_analysis) + + # checking if there is any unknown: + if missing_subdeps: + for subdep in missing_subdeps: + if not subdep[0]: + return [*missing_subdeps, *missing_root] + + if subdep[0] not in missing_names: + missing_sub.append(subdep) + return [*missing_sub, *missing_root] + + def get_missing_subdeps_of(self, names: Set[str], repository: str) -> List[Tuple[str, str]]: + missing = [] + already_added = {*names} + in_analyses = {*names} + + for name in names: + subdeps = self.aur_client.get_required_dependencies( + name) if repository == 'arch' else pacman.read_dependencies(name) + + if subdeps: + missing_subdeps = self.get_missing_packages(subdeps, in_analysis=in_analyses) + + if missing_subdeps: + for subdep in missing_subdeps: # checking if there is any unknown: + if subdep[0] not in already_added: + missing.append(subdep) + + if not subdep[0]: + return missing + return missing + + def get_missing_subdeps(self, name: str, repository: str, srcinfo: dict = None) -> List[Tuple[str, str]]: + missing = [] + already_added = {name} + in_analyses = {name} + + if repository == 'arch': + subdeps = self.aur_client.get_required_dependencies( + name) if not srcinfo else self.aur_client.extract_required_dependencies(srcinfo) + else: + subdeps = pacman.read_dependencies(name) + + if subdeps: + missing_subdeps = self.get_missing_packages(subdeps, in_analysis=in_analyses) + + if missing_subdeps: + for subdep in missing_subdeps: # checking if there is any unknown: + if subdep[0] not in already_added: + missing.append(subdep) + + if not subdep[0]: + return missing + return missing + + def map_known_missing_deps(self, known_deps: Dict[str, str], watcher: ProcessWatcher, check_subdeps: bool = True) -> \ + List[Tuple[str, str]]: + sorted_deps = [] # it will hold the proper order to install the missing dependencies + + repo_deps, aur_deps = set(), set() + + for dep, repo in known_deps.items(): + if repo == 'arch': + aur_deps.add(dep) + else: + repo_deps.add(dep) + + if check_subdeps: + for deps in ((repo_deps, 'repo'), (aur_deps, 'arch')): + if deps[0]: + missing_subdeps = self.get_missing_subdeps_of(deps[0], deps[1]) + + if missing_subdeps: + for dep in missing_subdeps: + if not dep[1]: + message.show_dep_not_found(dep[0], self.i18n, watcher) + return + + for dep in missing_subdeps: + if dep not in sorted_deps: + sorted_deps.append(dep) + + for dep, repo in known_deps.items(): + if repo != 'arch': + data = (dep, repo) + if data not in sorted_deps: + sorted_deps.append(data) + + for dep in aur_deps: + sorted_deps.append((dep, 'arch')) + + return sorted_deps + + def _fill_missing_dep(self, dep_name: str, dep_exp: str, aur_index: Iterable[str], + missing_deps: Set[Tuple[str, str]], + remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], + repo_deps: Set[str], aur_deps: Set[str], deps_data: Dict[str, dict], watcher: ProcessWatcher): + + if dep_name == dep_exp: + providers = remote_provided_map.get(dep_name) + else: # handling cases when the dep has an expression ( e.g: xpto>=0.12 ) + providers = remote_provided_map.get(dep_exp) + + if providers is None: + providers = remote_provided_map.get(dep_name) + + if providers and len(providers) > 1: + no_mapped_data = {p for p in providers if + p not in deps_data} # checking providers with no mapped data + + if no_mapped_data: + providers_data = pacman.map_updates_data(no_mapped_data) + + if not providers_data: + raise Exception("Could not retrieve the info from providers: {}".format(no_mapped_data)) + + deps_data.update(providers_data) # adding missing providers data + + matched_providers = set() + split_informed_dep = self.re_dep_operator.split(dep_exp) + version_informed = LooseVersion(split_informed_dep[2]) + exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' + + for p in providers: + provided = deps_data[p]['p'] + + for provided_exp in provided: + split_dep = self.re_dep_operator.split(provided_exp) + + if len(split_dep) == 3 and split_dep[0] == dep_name: + provided_version = LooseVersion(split_dep[2]) + + if eval('provided_version {} version_informed'.format(exp_op)): + matched_providers.add(p) + break + + providers = matched_providers + + if providers: + if len(providers) > 1: + dep_data = (dep_name, '__several__') + else: + real_name = providers.pop() + dep_data = (real_name, remote_repo_map.get(real_name)) + + repo_deps.add(dep_data[0]) + missing_deps.add(dep_data) + + elif aur_index and dep_name in aur_index: + aur_deps.add(dep_name) + missing_deps.add((dep_name, 'arch')) + else: + if watcher: + message.show_dep_not_found(dep_name, self.i18n, watcher) + raise PackageNotFoundException(dep_name) + else: + raise PackageNotFoundException(dep_name) + + def __fill_aur_update_data(self, pkgname: str, output: dict): + output[pkgname] = self.aur_client.map_update_data(pkgname, None) + + def map_missing_deps(self, pkgs_data: Dict[str, dict], provided_map: Dict[str, Set[str]], + remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], + aur_index: Iterable[str], deps_checked: Set[str], deps_data: Dict[str, dict], + sort: bool, watcher: ProcessWatcher, choose_providers: bool = True) -> List[Tuple[str, str]]: + sorted_deps = [] # it will hold the proper order to install the missing dependencies + + missing_deps, repo_missing, aur_missing = set(), set(), set() + + deps_checked.update(pkgs_data.keys()) + + for p, data in pkgs_data.items(): + if data['d']: + for dep in data['d']: + if dep in pkgs_data: + continue + if dep not in provided_map: + dep_split = self.re_dep_operator.split(dep) + dep_name = dep_split[0].strip() + + if dep_name not in deps_checked: + deps_checked.add(dep_name) + + if dep_name not in provided_map: + self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, + missing_deps=missing_deps, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + repo_deps=repo_missing, aur_deps=aur_missing, watcher=watcher, + deps_data=deps_data) + else: + version_pattern = '{}='.format(dep_name) + version_found = [p for p in provided_map if p.startswith(version_pattern)] + + if version_found: + version_found = version_found[0].split('=')[1] + version_informed = dep_split[2].strip() + + if ':' not in version_informed: + version_found = version_found.split(':')[-1] + + if '-' not in version_informed: + version_found = version_found.split('-')[0] + + version_found = LooseVersion(version_found) + version_informed = LooseVersion(version_informed) + + op = dep_split[1] if dep_split[1] != '=' else '==' + if not eval('version_found {} version_informed'.format(op)): + self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, + missing_deps=missing_deps, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + repo_deps=repo_missing, aur_deps=aur_missing, + watcher=watcher, + deps_data=deps_data) + else: + self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, + missing_deps=missing_deps, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + repo_deps=repo_missing, aur_deps=aur_missing, + watcher=watcher, + deps_data=deps_data) + + if missing_deps: + if repo_missing: + with_single_providers = [] + + for d in missing_deps: + if d[0] in repo_missing and d[0] not in deps_data: + if d[1] == '__several__': + deps_data[d[0]] = {'d': None, 'p': d[0], 'r': d[1]} + else: + with_single_providers.append(d[0]) + + if with_single_providers: + data = pacman.map_updates_data(with_single_providers) + + if data: + deps_data.update(data) + + if aur_missing: + aur_threads = [] + for pkgname in aur_missing: + t = Thread(target=self.__fill_aur_update_data, args=(pkgname, deps_data), daemon=True) + t.start() + aur_threads.append(t) + + for t in aur_threads: + t.join() + + missing_subdeps = self.map_missing_deps(pkgs_data=deps_data, provided_map=provided_map, aur_index=aur_index, + deps_checked=deps_checked, sort=False, deps_data=deps_data, + watcher=watcher, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + choose_providers=False) + + if missing_subdeps: + missing_deps.update(missing_subdeps) + + if sort: + sorted_deps.extend(sorting.sort(deps_data.keys(), deps_data)) + else: + sorted_deps.extend(((dep[0], dep[1]) for dep in missing_deps)) + + if sorted_deps and choose_providers: + return self.fill_providers_deps(missing_deps=sorted_deps, provided_map=provided_map, + remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map, + watcher=watcher, sort=sort, already_checked=deps_checked, + aur_idx=aur_index, deps_data=deps_data) + + return sorted_deps + + def fill_providers_deps(self, missing_deps: List[Tuple[str, str]], + provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], + already_checked: Set[str], remote_provided_map: Dict[str, Set[str]], + deps_data: Dict[str, dict], aur_idx: Iterable[str], sort: bool, + watcher: ProcessWatcher) -> List[Tuple[str, str]]: + """ + :param missing_deps: + :param provided_map: + :param remote_repo_map: + :param already_checked: + :param remote_provided_map: + :param deps_data: + :param aur_idx: + :param sort: + :param watcher: + :return: all deps sorted or None if the user declined the providers options + """ + + deps_providers = map_providers({data[0] for data in missing_deps if data[1] == '__several__'}, + remote_provided_map) + + if deps_providers: + all_providers = set() + + for providers in deps_providers.values(): + all_providers.update(providers) + + providers_repos = pacman.map_repositories(all_providers) + selected_providers = confirmation.request_providers(deps_providers, providers_repos, watcher, self.i18n) + + if not selected_providers: + return + else: + providers_data = pacman.map_updates_data( + selected_providers) # adding the chosen providers to re-check the missing deps + provided_map.update(pacman.map_provided(remote=True, + pkgs=selected_providers)) # adding the providers as "installed" packages + + providers_deps = self.map_missing_deps(pkgs_data=providers_data, + provided_map=provided_map, + aur_index=aur_idx, + deps_checked=already_checked, + deps_data=deps_data, + sort=False, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + watcher=watcher, + choose_providers=False) + + # cleaning the already mapped providers deps: + to_remove = [] + + for idx, dep in enumerate(missing_deps): + if dep[1] == '__several__': + to_remove.append(idx) + + for idx, to_remove in enumerate(to_remove): + del missing_deps[to_remove - idx] + + missing_deps.extend(((p, providers_repos.get(p, 'arch')) for p in selected_providers)) + + for dep in providers_deps: + if dep not in missing_deps and dep[1] != '__several__': + missing_deps.append(dep) + + deps_data.update(providers_data) + + if not self.fill_providers_deps(missing_deps=missing_deps, provided_map=provided_map, + remote_repo_map=remote_repo_map, already_checked=already_checked, + aur_idx=aur_idx, remote_provided_map=remote_provided_map, + deps_data=deps_data, sort=False, watcher=watcher): + return + + if sort: + missing_to_sort = {d[0] for d in missing_deps if d[1] != '__several__'} + return sorting.sort(missing_to_sort, deps_data, provided_map) + + return missing_deps + + def map_all_required_by(self, pkgnames: Iterable[str], to_ignore: Set[str]) -> Set[str]: + to_ignore.update(pkgnames) + all_requirements = {req for reqs in pacman.map_required_by(pkgnames).values() for req in reqs if req not in to_ignore} + + if all_requirements: + sub_requirements = self.map_all_required_by(all_requirements, to_ignore) + + if sub_requirements: + all_requirements.update(sub_requirements) + return all_requirements + + return all_requirements + + +def map_providers(pkgs: Iterable[str], remote_provided_map: Dict[str, Set[str]]) -> Dict[str, Set[str]]: + res = {} + + for p in pkgs: + providers = remote_provided_map.get(p) + + if providers and len(providers) > 1: + res[p] = providers + + return res diff --git a/bauh/gems/arch/disk.py b/bauh/gems/arch/disk.py index 124a6a66..fbb7fb1a 100644 --- a/bauh/gems/arch/disk.py +++ b/bauh/gems/arch/disk.py @@ -2,62 +2,73 @@ import json import os import re from pathlib import Path -from typing import Set, List +from typing import List, Dict, Iterable from bauh.gems.arch import pacman from bauh.gems.arch.model import ArchPackage -RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon)\s*=\s*(.+)') -RE_CLEAN_NAME = re.compile(r'^(\w+)-?|_?.+') +RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon|NoDisplay)\s*=\s*(.+)') +RE_CLEAN_NAME = re.compile(r'[+*?%]') -def write(app: ArchPackage): - data = app.get_data_to_cache() +def write(pkg: ArchPackage): + data = pkg.get_data_to_cache() if data: - Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) + Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) - with open(app.get_disk_data_path(), 'w+') as f: + with open(pkg.get_disk_data_path(), 'w+') as f: f.write(json.dumps(data)) -def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool): - ends_with = re.compile(r'.+/{}\.(png|svg|xpm)$'.format(app.icon_path if app.icon_path else app.name), re.IGNORECASE) +def fill_icon_path(pkg: ArchPackage, icon_paths: List[str], only_exact_match: bool): + clean_name = RE_CLEAN_NAME.sub('', pkg.name) + ends_with = re.compile(r'.+/{}\.(png|svg|xpm)$'.format(pkg.icon_path if pkg.icon_path else clean_name), re.IGNORECASE) for path in icon_paths: if ends_with.match(path): - app.icon_path = path + pkg.icon_path = path return if not only_exact_match: - pkg_icons_path = pacman.list_icon_paths({app.name}) + pkg_icons_path = pacman.list_icon_paths({pkg.name}) if pkg_icons_path: - app.set_icon(pkg_icons_path) + pkg.set_icon(pkg_icons_path) -def set_icon_path(app: ArchPackage, icon_name: str = None): - installed_icons = pacman.list_icon_paths({app.name}) +def set_icon_path(pkg: ArchPackage, icon_name: str = None): + installed_icons = pacman.list_icon_paths({pkg.name}) if installed_icons: - exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else app.name)) + exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else pkg.name)) for icon_path in installed_icons: if exact_match.match(icon_path): - app.icon_path = icon_path + pkg.icon_path = icon_path break -def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, maintainer: str = None, categories: dict = None) -> int: - to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))} +def save_several(pkgnames: Iterable[str], repo_map: Dict[str, str], overwrite: bool = True, maintainer: str = None, + categories: dict = None, when_prepared=None, after_written=None) -> int: + if overwrite: + to_cache = pkgnames + else: + to_cache = {n for n in pkgnames if not os.path.exists(ArchPackage.disk_cache_path(n))} + desktop_files = pacman.list_desktop_entries(to_cache) - no_desktop_files = {} + no_desktop_files = set() to_write = [] + if desktop_files: desktop_matches, no_exact_match = {}, set() for pkg in to_cache: # first try to find exact matches - ends_with = re.compile('/usr/share/applications/{}.desktop$'.format(pkg), re.IGNORECASE) + try: + clean_name = RE_CLEAN_NAME.sub('', pkg) + ends_with = re.compile(r'/usr/share/applications/{}.desktop$'.format(clean_name), re.IGNORECASE) + except: + raise for f in desktop_files: if ends_with.match(f): @@ -88,23 +99,35 @@ def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, mainta pkgs, apps_icons_noabspath = [], [] for pkgname, file in desktop_matches.items(): - p = ArchPackage(name=pkgname, mirror=mirror) - p.desktop_entry = file + p = ArchPackage(name=pkgname, repository=repo_map.get(pkgname)) with open(file) as f: - desktop_entry = f.read() + try: + desktop_entry = f.read() + p.desktop_entry = file - for field in RE_DESKTOP_ENTRY.findall(desktop_entry): - if field[0] == 'Exec': - p.command = field[1].strip().replace('"', '') - elif field[0] == 'Icon': - p.icon_path = field[1].strip() + for field in RE_DESKTOP_ENTRY.findall(desktop_entry): + if field[0] == 'Exec': + p.command = field[1].strip().replace('"', '') + elif field[0] == 'Icon': + p.icon_path = field[1].strip() - if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined - apps_icons_noabspath.append(p) + if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined + apps_icons_noabspath.append(p) + elif field[0] == 'NoDisplay' and field[1].strip().lower() == 'true': + p.command = None + + if p.icon_path: + apps_icons_noabspath.remove(p.icon_path) + p.icon_path = None + except: + continue pkgs.append(p) + if when_prepared: + when_prepared(p.name) + if apps_icons_noabspath: icon_paths = pacman.list_icon_paths({app.name for app in apps_icons_noabspath}) if icon_paths: @@ -113,35 +136,54 @@ def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, mainta for p in pkgs: to_write.append(p) + else: + no_desktop_files = {*pkgnames} if no_desktop_files: - pkgs = {ArchPackage(name=n, mirror=mirror) for n in no_desktop_files} bin_paths = pacman.list_bin_paths(no_desktop_files) + icon_paths = pacman.list_icon_paths(no_desktop_files) - if bin_paths: - for p in pkgs: - ends_with = re.compile(r'.+/{}$'.format(p.name), re.IGNORECASE) + for n in no_desktop_files: + p = ArchPackage(name=n, repository=repo_map.get(n)) + + if bin_paths: + clean_name = RE_CLEAN_NAME.sub('', p.name) + ends_with = re.compile(r'.+/{}$'.format(clean_name), re.IGNORECASE) for path in bin_paths: if ends_with.match(path): p.command = path break - - icon_paths = pacman.list_icon_paths(no_desktop_files) - - if icon_paths: - for p in pkgs: + if icon_paths: fill_icon_path(p, icon_paths, only_exact_match=True) - for p in pkgs: to_write.append(p) + if when_prepared: + when_prepared(p.name) + if to_write: + written = set() for p in to_write: if categories: p.categories = categories.get(p.name) - p.maintainer = maintainer + if maintainer and not p.maintainer: + p.maintainer = maintainer + write(p) + + if after_written: + after_written(p.name) + + written.add(p.name) + + if len(to_write) != len(to_cache): + for n in pkgnames: + if n not in written: + Path(ArchPackage.disk_cache_path(n)).mkdir(parents=True, exist_ok=True) + if after_written: + after_written(n) + return len(to_write) return 0 diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index 3993d9a3..507e14ca 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -117,7 +117,7 @@ class ArchDataMapper: def map_api_data(self, apidata: dict, installed: dict, categories: dict) -> ArchPackage: data = installed.get(apidata.get('Name')) - app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur', i18n=self.i18n) + app = ArchPackage(name=apidata.get('Name'), installed=bool(data), repository='arch', i18n=self.i18n) app.status = PackageStatus.LOADING_DATA if categories: diff --git a/bauh/gems/arch/message.py b/bauh/gems/arch/message.py index 320cf23b..d3127b40 100644 --- a/bauh/gems/arch/message.py +++ b/bauh/gems/arch/message.py @@ -1,22 +1,31 @@ +from typing import Iterable + from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.view import MessageType from bauh.commons.html import bold from bauh.view.util.translation import I18n -def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: I18n): - watcher.show_message(title=i18n['error'], - body=i18n['arch.install.dependency.install.error'].format(bold(depname), bold(pkgname)), +def show_deps_not_installed(watcher: ProcessWatcher, pkgname: str, depnames: Iterable[str], i18n: I18n): + deps = ', '.join((bold(d) for d in depnames)) + watcher.show_message(title=i18n['error'].capitalize(), + body=i18n['arch.install.dependency.install.error'].format(deps, bold(pkgname)), type_=MessageType.ERROR) def show_dep_not_found(depname: str, i18n: I18n, watcher: ProcessWatcher): - watcher.show_message(title=i18n['arch.install.dep_not_found.title'], - body=i18n['arch.install.dep_not_found.body'].format(bold(depname)), + + body = '

{}

{}

{}

'.format(i18n['arch.install.dep_not_found.body.l1'].format(bold(depname)), + i18n['arch.install.dep_not_found.body.l2'], + i18n['arch.install.dep_not_found.body.l3']) + + watcher.show_message(title=i18n['arch.install.dep_not_found.title'].capitalize(), + body=body, type_=MessageType.ERROR) -def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: I18n): - watcher.show_message(title=i18n['error'], - body=i18n['arch.install.optdep.error'].format(bold(depname)), +def show_optdeps_not_installed(depnames: Iterable[str], watcher: ProcessWatcher, i18n: I18n): + deps = ', '.join((bold(d) for d in depnames)) + watcher.show_message(title=i18n['error'].capitalize(), + body=i18n['arch.install.optdep.error'].format(deps), type_=MessageType.ERROR) diff --git a/bauh/gems/arch/mirrors.py b/bauh/gems/arch/mirrors.py new file mode 100644 index 00000000..98cc0a19 --- /dev/null +++ b/bauh/gems/arch/mirrors.py @@ -0,0 +1,42 @@ +import logging +import os +import time +import traceback +from datetime import datetime +from logging import Logger +from pathlib import Path + +from bauh.api.constants import CACHE_PATH + +SYNC_FILE = '{}/arch/mirrors_sync'.format(CACHE_PATH) + + +def should_sync(logger: logging.Logger): + if os.path.exists(SYNC_FILE): + with open(SYNC_FILE) as f: + sync_file = f.read() + + try: + sync_time = datetime.fromtimestamp(int(sync_file)) + now = datetime.now() + + if now > sync_time and now.day != sync_time.day: + logger.info("Package databases synchronization out of date") + else: + msg = "Package databases already synchronized" + logger.info(msg) + return False + except: + logger.warning("Could not convert the database synchronization time from '{}".format(SYNC_FILE)) + traceback.print_exc() + return True + + +def register_sync(logger: Logger): + try: + Path('/'.join(SYNC_FILE.split('/')[0:-1])).mkdir(parents=True, exist_ok=True) + with open(SYNC_FILE, 'w+') as f: + f.write(str(int(time.time()))) + except: + logger.error("Could not write to mirrors sync file '{}'".format(SYNC_FILE)) + traceback.print_exc() diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index 37d3a3b7..24f6459e 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -6,7 +6,7 @@ from bauh.commons import resource from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH from bauh.view.util.translation import I18n -CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry', 'categories'} +CACHED_ATTRS = {'command', 'icon_path', 'repository', 'maintainer', 'desktop_entry', 'categories'} class ArchPackage(SoftwarePackage): @@ -14,7 +14,7 @@ class ArchPackage(SoftwarePackage): def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None, package_base: str = None, votes: int = None, popularity: float = None, first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None, - maintainer: str = None, url_download: str = None, pkgbuild: str = None, mirror: str = None, + maintainer: str = None, url_download: str = None, pkgbuild: str = None, repository: str = None, desktop_entry: str = None, installed: bool = False, srcinfo: dict = None, dependencies: Set[str] = None, i18n: I18n = None): @@ -22,12 +22,12 @@ class ArchPackage(SoftwarePackage): self.package_base = package_base self.votes = votes self.popularity = popularity - self.maintainer = maintainer + self.maintainer = maintainer if maintainer else (repository if repository != 'arch' else None) self.url_download = url_download self.first_submitted = first_submitted self.last_modified = last_modified self.pkgbuild = pkgbuild - self.mirror = mirror + self.repository = repository self.command = None self.icon_path = None self.downgrade_enabled = False @@ -37,27 +37,31 @@ class ArchPackage(SoftwarePackage): self.i18n = i18n @staticmethod - def disk_cache_path(pkgname: str, mirror: str): - return ARCH_CACHE_PATH + '/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname + def disk_cache_path(pkgname: str): + return ARCH_CACHE_PATH + '/installed/' + pkgname def get_pkg_build_url(self): if self.package_base: return 'https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=' + self.package_base def has_history(self): - return self.installed + return self.installed and self.repository == 'arch' def has_info(self): return True - def can_be_installed(self): - return super(ArchPackage, self).can_be_installed() and self.url_download + def can_be_installed(self) -> bool: + if super(ArchPackage, self).can_be_installed(): + return bool(self.url_download) if self.repository == 'arch' else True def can_be_downgraded(self): - return self.installed and self.downgrade_enabled + return self.installed and self.downgrade_enabled and self.repository == 'arch' def get_type(self): - return 'aur' if self.mirror == 'aur' else 'arch' + return 'arch' if self.repository == 'arch' else 'arch_repo' + + def get_update_type(self): + return 'Arch - {}'.format('AUR' if self.repository == 'arch' else 'Repository') def get_default_icon_path(self) -> str: return self.get_type_icon_path() @@ -66,7 +70,7 @@ class ArchPackage(SoftwarePackage): return self.icon_path def get_type_icon_path(self): - return resource.get_path('img/{}.svg'.format('arch' if self.mirror == 'aur' else 'mirror'), ROOT_DIR) + return resource.get_path('img/{}.svg'.format('arch' if self.get_type() == 'arch' else 'repo'), ROOT_DIR) def is_application(self): return self.can_be_run() @@ -79,7 +83,7 @@ class ArchPackage(SoftwarePackage): def get_disk_cache_path(self) -> str: if self.name: - return self.disk_cache_path(self.name, self.mirror) + return self.disk_cache_path(self.name) def get_data_to_cache(self) -> dict: cache = {} @@ -125,7 +129,10 @@ class ArchPackage(SoftwarePackage): return False def get_name_tooltip(self) -> str: - return '{} ( {}: {} )'.format(self.name, self.i18n['repository'], self.mirror) + return '{} ( {}: {} )'.format(self.name, self.i18n['repository'], self.repository) + + def supports_backup(self) -> bool: + return True def __str__(self): return self.__repr__() diff --git a/bauh/gems/arch/output.py b/bauh/gems/arch/output.py new file mode 100644 index 00000000..38b9b16f --- /dev/null +++ b/bauh/gems/arch/output.py @@ -0,0 +1,109 @@ +import logging +import time +from threading import Thread + +from bauh.api.abstract.handler import ProcessWatcher +from bauh.view.util.translation import I18n + + +class TransactionStatusHandler(Thread): + + def __init__(self, watcher: ProcessWatcher, i18n: I18n, npkgs: int, logger: logging.Logger, percentage: bool = True): + super(TransactionStatusHandler, self).__init__(daemon=True) + self.watcher = watcher + self.i18n = i18n + self.npkgs = npkgs + self.downloading = 0 + self.upgrading = 0 + self.installing = 0 + self.outputs = [] + self.work = True + self.logger = logger + self.percentage = percentage + self.accepted = {'checking keyring', + 'checking package integrity', + 'loading package files', + 'checking for file conflicts', + 'checking available disk space'} + + def gen_percentage(self) -> str: + if self.percentage: + performed = self.downloading + self.upgrading + self.installing + return '({0:.2f}%) '.format((performed / (2 * self.npkgs)) * 100) + else: + return '' + + def get_performed(self) -> int: + return self.upgrading + self.installing + + def _handle(self, output: str) -> bool: + if output: + if output.startswith('downloading'): + if self.downloading < self.npkgs: + perc = self.gen_percentage() + self.downloading += 1 + + self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, self.downloading, self.npkgs, + self.i18n['downloading'].capitalize(), output.split(' ')[1].strip())) + elif output.startswith('upgrading'): + self.downloading = self.npkgs # to avoid wrong numbers the packages are cached + + if self.get_performed() < self.npkgs: + perc = self.gen_percentage() + self.upgrading += 1 + + performed = self.upgrading + self.installing + + if performed <= self.npkgs: + self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, self.upgrading, self.npkgs, + self.i18n['manage_window.status.upgrading'].capitalize(), output.split(' ')[1].strip())) + elif output.startswith('installing'): + self.downloading = self.npkgs # to avoid wrong numbers the packages are cached + + if self.get_performed() < self.npkgs: + perc = self.gen_percentage() + self.installing += 1 + + performed = self.upgrading + self.installing + + if performed <= self.npkgs: + self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, self.installing, self.npkgs, + self.i18n['manage_window.status.installing'].capitalize(), + output.split(' ')[1].strip())) + else: + substatus_found = False + lower_output = output.lower() + for msg in self.accepted: + if lower_output.startswith(msg): + self.watcher.change_substatus(self.i18n['arch.substatus.{}'.format(msg)].capitalize()) + substatus_found = True + break + + if not substatus_found: + performed = self.get_performed() + + if performed == 0 and self.downloading > 0: + self.watcher.change_substatus('') + elif performed == self.npkgs: + self.watcher.change_substatus(self.i18n['finishing'].capitalize()) + return False + + return True + + def handle(self, output: str): + self.outputs.append(output) + + def stop_working(self): + self.work = False + + def run(self): + self.logger.info("Starting") + while self.work: + if self.outputs: + output = self.outputs.pop() + if not self._handle(output): + break + else: + time.sleep(0.005) + + self.logger.info("Finished") diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 9651e546..71cf11a9 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -1,62 +1,70 @@ +import os import re from threading import Thread -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Dict, Iterable -from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess +from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess, \ + ProcessHandler +from bauh.commons.util import size_to_byte from bauh.gems.arch.exceptions import PackageNotFoundException RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]') RE_OPTDEPS = re.compile(r'[\w\._\-]+\s*:') RE_DEP_NOTFOUND = re.compile(r'error:.+\'(.+)\'') RE_DEP_OPERATORS = re.compile(r'[<>=]') +RE_INSTALLED_FIELDS = re.compile(r'(Name|Description|Version|Validated By)\s*:\s*(.+)') +RE_INSTALLED_SIZE = re.compile(r'Installed Size\s*:\s*([0-9,\.]+)\s(\w+)\n?', re.IGNORECASE) +RE_UPDATE_REQUIRED_FIELDS = re.compile(r'(\bProvides\b|\bInstalled Size\b|\bConflicts With\b)\s*:\s(.+)\n') +RE_REMOVE_TRANSITIVE_DEPS = re.compile(r'removing\s([\w\-_]+)\s.+required\sby\s([\w\-_]+)\n?') -def is_enabled() -> bool: - res = run_cmd('which pacman') +def is_available() -> bool: + res = run_cmd('which pacman', print_error=False) return res and not res.strip().startswith('which ') -def get_repositories(pkgs: Set[str]) -> dict: +def get_repositories(pkgs: Iterable[str]) -> dict: pkgre = '|'.join(pkgs).replace('+', r'\+').replace('.', r'\.') searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout - mirrors = {} + repositories = {} for line in new_subprocess(['grep', '-E', '.+/({}) '.format(pkgre)], stdin=searchres).stdout: if line: match = line.decode() for p in pkgs: if p in match: - mirrors[p] = match.split('/')[0] + repositories[p] = match.split('/')[0] - not_found = {pkg for pkg in pkgs if pkg and pkg not in mirrors} + not_found = {pkg for pkg in pkgs if pkg and pkg not in repositories} if not_found: # if there are some packages not found, try to find via the single method: for dep in not_found: - mirror_data = guess_repository(dep) + repo_data = guess_repository(dep) - if mirror_data: - mirrors[mirror_data[0]] = mirror_data[1] + if repo_data: + repositories[repo_data[0]] = repo_data[1] - return mirrors + return repositories def is_available_in_repositories(pkg_name: str) -> bool: return bool(run_cmd('pacman -Ss ' + pkg_name)) -def get_info(pkg_name) -> str: - return run_cmd('pacman -Qi ' + pkg_name) +def get_info(pkg_name, remote: bool = False) -> str: + return run_cmd('pacman -{}i {}'.format('Q' if not remote else 'S', pkg_name)) -def get_info_list(pkg_name: str) -> List[tuple]: - info = get_info(pkg_name) +def get_info_list(pkg_name: str, remote: bool = False) -> List[tuple]: + info = get_info(pkg_name, remote) if info: return re.findall(r'(\w+\s?\w+)\s*:\s*(.+(\n\s+.+)*)', info) -def get_info_dict(pkg_name: str) -> dict: - info_list = get_info_list(pkg_name) +def get_info_dict(pkg_name: str, remote: bool = False) -> dict: + list_attrs = {'depends on', 'required by'} + info_list = get_info_list(pkg_name, remote) if info_list: info_dict = {} @@ -69,16 +77,12 @@ def get_info_dict(pkg_name: str) -> dict: if attr == 'optional deps' and info_dict[attr]: info_dict[attr] = info_dict[attr].split('\n') - elif attr == 'depends on' and info_dict[attr]: + elif attr in list_attrs and info_dict[attr]: info_dict[attr] = [d.strip() for d in info_dict[attr].split(' ') if d] return info_dict -def list_installed() -> Set[str]: - return {out.decode().strip() for out in new_subprocess(['pacman', '-Qq']).stdout if out} - - def check_installed(pkg: str) -> bool: res = run_cmd('pacman -Qq ' + pkg, print_error=False) return bool(res) @@ -88,55 +92,54 @@ def _fill_ignored(res: dict): res['pkgs'] = list_ignored_packages() -def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values - installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names - allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info - +def map_installed(repositories: bool = True, aur: bool = True) -> dict: # returns a dict with with package names as keys and versions as values ignored = {} - thread_ignored = Thread(target=_fill_ignored, args=(ignored,)) + thread_ignored = Thread(target=_fill_ignored, args=(ignored,), daemon=True) thread_ignored.start() - pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {} - for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], - stdin=allinfo).stdout: # filtering only the Name and Validated By fields: - if out: - line = out.decode() + allinfo = run_cmd('pacman -Qi') - if line.startswith('Name'): - current_pkg['name'] = line.split(':')[1].strip() - elif line.startswith('Version'): - version = line.split(':') - current_pkg['version'] = version[len(version) - 1].strip() - elif line.startswith('Description'): - current_pkg['description'] = line.split(':')[1].strip() - elif line.startswith('Validated'): + pkgs = {'signed': {}, 'not_signed': {}} + current_pkg = {} + for idx, field_tuple in enumerate(RE_INSTALLED_FIELDS.findall(allinfo)): + if field_tuple[0].startswith('N'): + current_pkg['name'] = field_tuple[1].strip() + elif field_tuple[0].startswith('Ve'): + current_pkg['version'] = field_tuple[1].split(':')[-1].strip() + elif field_tuple[0].startswith('D'): + current_pkg['description'] = field_tuple[1].strip() + elif field_tuple[0].startswith('Va'): + if field_tuple[1].strip().lower() == 'none' and aur: + pkgs['not_signed'][current_pkg['name']] = current_pkg + del current_pkg['name'] + elif repositories: + pkgs['signed'][current_pkg['name']] = current_pkg + del current_pkg['name'] - if line.split(':')[1].strip().lower() == 'none': - pkgs['not_signed'][current_pkg['name']] = {'version': current_pkg['version'], - 'description': current_pkg['description']} + current_pkg = {} - current_pkg = {} - - if pkgs and pkgs.get('not_signed'): + if pkgs['signed'] or pkgs['not_signed']: thread_ignored.join() if ignored['pkgs']: to_del = set() - for pkg in pkgs['not_signed'].keys(): - if pkg in ignored['pkgs']: - to_del.add(pkg) - for pkg in to_del: - del pkgs['not_signed'][pkg] + for key in ('signed', 'not_signed'): + if pkgs.get(key): + for pkg in pkgs[key].keys(): + if pkg in ignored['pkgs']: + to_del.add(pkg) + for pkg in to_del: + del pkgs[key][pkg] return pkgs -def install_as_process(pkgpath: str, root_password: str, aur: bool, pkgdir: str = '.') -> SystemProcess: - if aur: - cmd = ['pacman', '-U', pkgpath, '--noconfirm'] # pkgpath = install file path +def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, pkgdir: str = '.') -> SystemProcess: + if file: + cmd = ['pacman', '-U', *pkgpaths, '--noconfirm'] # pkgpath = install file path else: - cmd = ['pacman', '-S', pkgpath, '--noconfirm'] # pkgpath = pkgname + cmd = ['pacman', '-S', *pkgpaths, '--noconfirm'] # pkgpath = pkgname return SystemProcess(new_root_subprocess(cmd, root_password, cwd=pkgdir), wrong_error_phrase='warning:') @@ -261,20 +264,19 @@ def read_repository_from_info(name: str) -> str: if not_found: return - mirror = None + repository = None for o in new_subprocess(['grep', '-Po', "Repository\s+:\s+\K.+"], stdin=info.stdout).stdout: if o: line = o.decode().strip() if line: - mirror = line + repository = line - return mirror + return repository def guess_repository(name: str) -> Tuple[str, str]: - if not name: raise Exception("'name' cannot be None or blank") @@ -366,3 +368,474 @@ def get_version_for_not_installed(pkgname: str) -> str: if output: return output.split('\n')[0].split(' ')[1].strip() + + +def map_repositories(pkgnames: Iterable[str] = None) -> Dict[str, str]: + info = run_cmd('pacman -Si {}'.format(' '.join(pkgnames) if pkgnames else ''), print_error=False, ignore_return_code=True) + if info: + repos = re.findall(r'(Name|Repository)\s*:\s*(.+)', info) + + if repos: + return {repos[idx+1][1].strip(): repo_data[1].strip() for idx, repo_data in enumerate(repos) if idx % 2 == 0} + + return {} + + +def list_repository_updates() -> Dict[str, str]: + output = run_cmd('pacman -Qu') + res = {} + if output: + for line in output.split('\n'): + if line: + line_split = line.split(' ') + res[line_split[0]] = line_split[-1] + return res + + +def map_sorting_data(pkgnames: List[str]) -> Dict[str, dict]: + allinfo = new_subprocess(['pacman', '-Qi', *pkgnames]).stdout + + pkgs, current_pkg = {}, {} + mapped_attrs = 0 + for out in new_subprocess(["grep", "-Po", "(Name|Provides|Depends On)\s*:\s*\K(.+)"], stdin=allinfo).stdout: + if out: + line = out.decode().strip() + + if line: + if mapped_attrs == 0: + current_pkg['name'] = line + elif mapped_attrs == 1: + provides = set() if line == 'None' else set(line.split(' ')) + provides.add(current_pkg['name']) + current_pkg['provides'] = provides + elif mapped_attrs == 2: + current_pkg['depends'] = line.split(':')[1].strip() + pkgs[current_pkg['name']] = current_pkg + del current_pkg['name'] + + mapped_attrs = 0 + current_pkg = {} + return pkgs + + +def get_build_date(pkgname: str) -> str: + output = run_cmd('pacman -Qi {}'.format(pkgname)) + + if output: + bdate_line = [l for l in output.split('\n') if l.startswith('Build Date')] + + if bdate_line: + return ':'.join(bdate_line[0].split(':')[1:]).strip() + + +def search(words: str) -> Dict[str, dict]: + output = run_cmd('pacman -Ss ' + words) + + if output: + found, current = {}, {} + for l in output.split('\n'): + if l: + if l.startswith(' '): + current['description'] = l.strip() + found[current['name']] = current + del current['name'] + current = None + else: + if current is None: + current = {} + + repo_split = l.split('/') + current['repository'] = repo_split[0] + + data_split = repo_split[1].split(' ') + current['name'] = data_split[0] + + version = data_split[1].split(':') + current['version'] = version[0] if len(version) == 1 else version[1] + return found + + +def get_databases() -> Set[str]: + with open('/etc/pacman.conf') as f: + conf_str = f.read() + + return {db for db in re.findall(r'[\n|\s]+\[(\w+)\]', conf_str) if db != 'options'} + + +def can_refresh_mirrors() -> bool: + output = run_cmd('which pacman-mirrors', print_error=False) + return True if output else False + + +def refresh_mirrors(root_password: str) -> SimpleProcess: + return SimpleProcess(cmd=['pacman-mirrors', '-g'], root_password=root_password) + + +def update_mirrors(root_password: str, countries: List[str]) -> SimpleProcess: + return SimpleProcess(cmd=['pacman-mirrors', '-c', ','.join(countries)], root_password=root_password) + + +def sort_fastest_mirrors(root_password: str, limit: int) -> SimpleProcess: + cmd = ['pacman-mirrors', '--fasttrack'] + + if limit > 0: + cmd.append(str(limit)) + + return SimpleProcess(cmd=cmd, root_password=root_password) + + +def list_mirror_countries() -> List[str]: + output = run_cmd('pacman-mirrors -l') + + if output: + return [c for c in output.split('\n') if c] + + +def get_current_mirror_countries() -> List[str]: + output = run_cmd('pacman-mirrors -lc').strip() + return ['all'] if not output else [c for c in output.split('\n') if c] + + +def is_mirrors_available() -> bool: + res = run_cmd('which pacman-mirrors', print_error=False) + return res and not res.strip().startswith('which ') + + +def get_update_size(pkgs: List[str]) -> Dict[str, int]: # bytes: + output = run_cmd('pacman -Si {}'.format(' '.join(pkgs))) + + if output: + return {pkgs[idx]: size_to_byte(float(size[0]), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))} + + return {} + + +def get_installed_size(pkgs: List[str]) -> Dict[str, int]: # bytes + output = run_cmd('pacman -Qi {}'.format(' '.join(pkgs))) + + if output: + return {pkgs[idx]: size_to_byte(float(size[0]), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))} + + return {} + + +def upgrade_system(root_password: str) -> SimpleProcess: + return SimpleProcess(cmd=['pacman', '-Syyu', '--noconfirm'], root_password=root_password) + + +def get_dependencies_to_remove(pkgs: Iterable[str], root_password: str) -> Dict[str, str]: + proc = SimpleProcess(cmd=['pacman', '-R', *pkgs, '--confirm'], root_password=root_password) + success, output = ProcessHandler().handle_simple(proc) + + if not output: + return {} + + return {t[1]: t[0] for t in RE_REMOVE_TRANSITIVE_DEPS.findall(output)} + + +def fill_provided_map(key: str, val: str, output: dict): + current_val = output.get(key) + + if current_val is None: + output[key] = {val} + else: + current_val.add(val) + + +def map_provided(remote: bool = False, pkgs: Iterable[str] = None) -> Dict[str, Set[str]]: + output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(pkgs) if pkgs else '')) + + if output: + provided_map = {} + latest_name, latest_version, provided = None, None, False + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + val = line[field_sep_idx + 1:].strip() + + if field == 'Name': + latest_name = val + elif field == 'Version': + latest_version = val.split('=')[0] + elif field == 'Provides': + fill_provided_map(latest_name, latest_name, provided_map) + fill_provided_map('{}={}'.format(latest_name, latest_version), latest_name, provided_map) + + if val != 'None': + for w in val.split(' '): + if w: + word = w.strip() + fill_provided_map(word, latest_name, provided_map) + + word_split = word.split('=') + + if word_split[0] != word: + fill_provided_map(word_split[0], latest_name, provided_map) + else: + provided = True + + elif provided: + latest_name = None + latest_version = None + provided = False + + elif provided: + for w in l.split(' '): + if w: + word = w.strip() + fill_provided_map(word, latest_name, provided_map) + + word_split = word.split('=') + + if word_split[0] != word: + fill_provided_map(word_split[0], latest_name, provided_map) + + return provided_map + + +def map_updates_data(pkgs: Iterable[str], files: bool = False) -> dict: + if files: + output = run_cmd('pacman -Qi -p {}'.format(' '.join(pkgs))) + else: + output = run_cmd('pacman -Si {}'.format(' '.join(pkgs))) + + if output: + res = {} + latest_name = None + data = {'ds': None, 's': None, 'v': None, 'c': None, 'p': None, 'd': None, 'r': None} + latest_field = None + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + val = line[field_sep_idx + 1:].strip() + + if field == 'Repository': + data['r'] = val + latest_field = 'r' + elif field == 'Name': + latest_name = val + latest_field = 'n' + elif field == 'Version': + data['v'] = val.split('=')[0] + latest_field = 'v' + elif field == 'Provides': + latest_field = 'p' + data['p'] = {latest_name, '{}={}'.format(latest_name, data['v'])} + if val != 'None': + for w in val.split(' '): + if w: + word = w.strip() + data['p'].add(word) + + word_split = word.split('=') + + if word_split[0] != word: + data['p'].add(word_split[0]) + elif field == 'Depends On': + val = val.strip() + + if val == 'None': + data['d'] = None + else: + data['d'] = {w.strip().split(':')[0].strip() for w in val.split(' ') if w} + latest_field = 'd' + elif field == 'Conflicts With': + if val == 'None': + data['c'] = None + else: + data['c'] = {w.strip() for w in val.split(' ') if w} + + latest_field = 'c' + elif field == 'Download Size': + size = val.split(' ') + data['ds'] = size_to_byte(float(size[0]), size[1]) + latest_field = 'ds' + elif field == 'Installed Size': + size = val.split(' ') + data['s'] = size_to_byte(float(size[0]), size[1]) + latest_field = 's' + elif latest_name and latest_field == 's': + res[latest_name] = data + latest_name = None + latest_field = None + data = {'ds': None, 's': None, 'c': None, 'p': None, 'd': None, 'r': None, 'v': None} + else: + latest_field = None + + elif latest_field and latest_field in ('p', 'c', 'd'): + if latest_field == 'p': + for w in l.split(' '): + if w: + word = w.strip() + data['p'].add(word) + + word_split = word.split('=') + + if word_split[0] != word: + data['p'].add(word_split[0]) + else: + data[latest_field].update((w.strip() for w in l.split(' ') if w)) + + return res + + +def list_installed_names() -> Set[str]: + return {p for p in run_cmd('pacman -Qq').split('\n') if p} + + +def upgrade_several(pkgnames: Iterable[str], root_password: str) -> SystemProcess: + cmd = ['pacman', '-S', *pkgnames, '--noconfirm'] + if root_password: + return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:') + else: + return SystemProcess(new_subprocess(cmd), wrong_error_phrase='warning:') + + +def remove_several(pkgnames: Iterable[str], root_password: str) -> SystemProcess: + cmd = ['pacman', '-R', *pkgnames, '--noconfirm'] + if root_password: + return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:') + else: + return SystemProcess(new_subprocess(cmd), wrong_error_phrase='warning:') + + +def map_optional_deps(names: Iterable[str], remote: bool, not_installed: bool = False) -> Dict[str, Dict[str, str]]: + output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(names))) + + if output: + res = {} + latest_name, deps = None, None + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + + if field == 'Name': + val = line[field_sep_idx + 1:].strip() + latest_name = val + elif field == 'Optional Deps': + val = line[field_sep_idx + 1:].strip() + deps = {} + if val != 'None': + if ':' in val: + dep_info = val.split(':') + desc = dep_info[1].strip() + + if desc and not_installed and '[installed]' in desc: + continue + + deps[dep_info[0].strip()] = desc + else: + sev_deps = {dep.strip(): '' for dep in val.split(' ') if dep and (not not_installed or '[installed]' not in dep)} + deps.update(sev_deps) + elif latest_name and deps is not None: + res[latest_name] = deps + latest_name, deps = None, None + + elif latest_name and deps is not None: + if ':' in l: + dep_info = l.split(':') + desc = dep_info[1].strip() + + if desc and not_installed and '[installed]' in desc: + continue + + deps[dep_info[0].strip()] = desc + else: + sev_deps = {dep.strip(): '' for dep in l.split(' ') if dep and (not not_installed or '[installed]' not in dep)} + deps.update(sev_deps) + + return res + + +def get_cache_dir() -> str: + dir_pattern = re.compile(r'.*CacheDir\s*=\s*.+') + + if os.path.exists('/etc/pacman.conf'): + with open('/etc/pacman.conf') as f: + config_str = f.read() + + cache_dirs = [] + + for string in dir_pattern.findall(config_str): + if not string.strip().startswith('#'): + cache_dirs.append(string.split('=')[1].strip()) + + return cache_dirs[-1] if cache_dirs else '/var/cache/pacman/pkg/' + + +def map_required_by(names: Iterable[str]) -> Dict[str, Set[str]]: + output = run_cmd('pacman -Qi {}'.format(' '.join(names))) + + if output: + res = {} + latest_name, required = None, None + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + + if field == 'Name': + val = line[field_sep_idx + 1:].strip() + latest_name = val + elif field == 'Required By': + val = line[field_sep_idx + 1:].strip() + required = set() + if val != 'None': + required.update((d for d in val.split(' ') if d)) + + elif latest_name and required is not None: + res[latest_name] = required + latest_name, required = None, None + + elif latest_name and required is not None: + required.update(required.update((d for d in l.strip().split(' ') if d))) + + return res + + +def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Set[str]]: + output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(names))) + + if output: + res = {} + latest_name, conflicts = None, None + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + + if field == 'Name': + val = line[field_sep_idx + 1:].strip() + latest_name = val + elif field == 'Conflicts With': + val = line[field_sep_idx + 1:].strip() + conflicts = set() + if val != 'None': + conflicts.update((d for d in val.split(' ') if d)) + + elif latest_name and conflicts is not None: + res[latest_name] = conflicts + latest_name, conflicts = None, None + + elif latest_name and conflicts is not None: + conflicts.update(conflicts.update((d for d in l.strip().split(' ') if d))) + + return res diff --git a/bauh/gems/arch/resources/img/mirror.svg b/bauh/gems/arch/resources/img/repo.svg similarity index 98% rename from bauh/gems/arch/resources/img/mirror.svg rename to bauh/gems/arch/resources/img/repo.svg index 246e8690..d0efa973 100644 --- a/bauh/gems/arch/resources/img/mirror.svg +++ b/bauh/gems/arch/resources/img/repo.svg @@ -14,7 +14,7 @@ viewBox="0 0 512.00003 512" width="512" xml:space="preserve" - sodipodi:docname="mirror.svg" + sodipodi:docname="repo.svg" inkscape:version="0.92.4 5da689c313, 2019-01-14">image/svg+xml idx: + idx = dep_idx + 1 + else: + real_dep = provided_map.get(dep) # gets the real package name instead of the provided one + + if not real_dep or real_dep not in pkgs_data: + continue # it means this depends does not belong to the sorting context + else: + dep_idx = sorted_names.get(real_dep) + + if dep_idx is not None and dep_idx + 1 > idx: + idx = dep_idx + 1 + else: + dep_idx = __add_dep_to_sort(real_dep, pkgs_data, sorted_names, not_sorted, provided_map) + if dep_idx + 1 > idx: + idx = dep_idx + 1 + + sorted_names[pkgname] = idx + return idx + + +def sort(pkgs: Iterable[str], pkgs_data: Dict[str, dict], provided_map: Dict[str, Set[str]] = None) -> List[Tuple[str, str]]: + sorted_list, sorted_names, not_sorted = [], set(), set() + provided = provided_map if provided_map else {} + + # add all packages with no dependencies first + for pkgname in pkgs: + data = pkgs_data[pkgname] + if not provided_map and data['p']: # mapping provided if reeded + for p in data['p']: + provided[p] = {pkgname} + + if not data['d']: + sorted_list.append(pkgname) + sorted_names.add(pkgname) + else: + not_sorted.add(pkgname) + + deps_map, not_deps_available = {}, set() + for pkg in not_sorted: # generating a dependency map with only the dependencies among the informed packages + pkgsdeps = set() + data = pkgs_data[pkg] + for dep in data['d']: + providers = provided.get(dep) + + if providers: + for p in providers: + if p in pkgs: + pkgsdeps.add(p) + + if pkgsdeps: + deps_map[pkg] = pkgsdeps + else: + not_deps_available.add(pkg) + sorted_list.append(pkg) + sorted_names.add(pkg) + + for pkg in not_deps_available: # removing from not_sorted + not_sorted.remove(pkg) + + while not_sorted: + sorted_in_round = set() + + for pkg in not_sorted: + idx = _index_pkg(pkg, sorted_list, sorted_names, deps_map, ignore_not_sorted=False) + + if idx >= 0: + sorted_in_round.add(pkg) + sorted_names.add(pkg) + sorted_list.insert(idx, pkg) + + for pkg in sorted_in_round: + not_sorted.remove(pkg) + + if not_sorted and not sorted_in_round: # it means there are cyclic deps + break + + if not_sorted: # it means there are cyclic deps + # filtering deps already mapped + for pkg in not_sorted: + deps_map[pkg] = deps_map[pkg].difference(sorted_names) + + dep_lvl_map = {} # holds the diff between the number of dependents per package and its dependencies + for pkg in not_sorted: + dependents = 0 + for pkg2 in not_sorted: + if pkg != pkg2 and pkg in deps_map[pkg2]: + dependents += 1 + + dep_lvl_map[pkg] = dependents - len(deps_map[pkg]) + + sorted_by_less_deps = [*not_sorted] + sorted_by_less_deps.sort(key=lambda o: dep_lvl_map[o], reverse=True) # sorting by higher dep level + + for pkg in sorted_by_less_deps: + idx = _index_pkg(pkg, sorted_list, sorted_names, deps_map, ignore_not_sorted=True) + sorted_names.add(pkg) + sorted_list.insert(idx, pkg) + + # putting arch pkgs in the end: + aur_pkgs = None + res = [] + + for name in sorted_list: + repo = pkgs_data[name]['r'] + if repo == 'arch': + if not aur_pkgs: + aur_pkgs = [] + + aur_pkgs.append((name, 'arch')) + else: + res.append((name, repo)) + + if aur_pkgs: + res.extend(aur_pkgs) + + return res + + +def _index_pkg(name: str, sorted_list: List[str], sorted_names: Set[str], deps_map: Dict[str, Set[str]], ignore_not_sorted: bool) -> int: + deps_to_check_idx = set() + for dep in deps_map[name]: + if dep in sorted_names: + deps_to_check_idx.add(dep) + elif not ignore_not_sorted: + return -1 + + if not deps_to_check_idx: + return len(sorted_list) + else: + idxs = {sorted_list.index(dep) for dep in deps_to_check_idx} + return max(idxs) + 1 diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py new file mode 100644 index 00000000..29b674dd --- /dev/null +++ b/bauh/gems/arch/updates.py @@ -0,0 +1,425 @@ +import logging +import time +from threading import Thread +from typing import Dict, Set, List, Tuple, Iterable + +from bauh.api.abstract.controller import UpgradeRequirements, UpgradeRequirement +from bauh.api.abstract.handler import ProcessWatcher +from bauh.gems.arch import pacman, sorting +from bauh.gems.arch.aur import AURClient +from bauh.gems.arch.dependencies import DependenciesAnalyser +from bauh.gems.arch.exceptions import PackageNotFoundException +from bauh.gems.arch.model import ArchPackage +from bauh.view.util.translation import I18n + + +class UpdateRequirementsContext: + + def __init__(self, to_update: Dict[str, ArchPackage], repo_to_update: Dict[str, ArchPackage], + aur_to_update: Dict[str, ArchPackage], repo_to_install: Dict[str, ArchPackage], + aur_to_install: Dict[str, ArchPackage], to_install: Dict[str, ArchPackage], + pkgs_data: Dict[str, dict], cannot_upgrade: Dict[str, UpgradeRequirement], + to_remove: Dict[str, UpgradeRequirement], installed_names: Set[str], provided_map: Dict[str, Set[str]], + aur_index: Set[str], arch_config: dict, remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], + root_password: str): + self.to_update = to_update + self.repo_to_update = repo_to_update + self.aur_to_update = aur_to_update + self.repo_to_install = repo_to_install + self.aur_to_install = aur_to_install + self.pkgs_data = pkgs_data + self.cannot_upgrade = cannot_upgrade + self.root_password = root_password + self.installed_names = installed_names + self.provided_map = provided_map + self.to_remove = to_remove + self.to_install = to_install + self.aur_index = aur_index + self.arch_config = arch_config + self.remote_provided_map = remote_provided_map + self.remote_repo_map = remote_repo_map + + +class UpdatesSummarizer: + + def __init__(self, aur_client: AURClient, i18n: I18n, logger: logging.Logger, deps_analyser: DependenciesAnalyser, watcher: ProcessWatcher): + self.aur_client = aur_client + self.i18n = i18n + self.logger = logger + self.watcher = watcher + self.deps_analyser = deps_analyser + + def _fill_aur_pkg_update_data(self, pkg: ArchPackage, output: dict): + output[pkg.name] = self.aur_client.map_update_data(pkg.get_base_name(), pkg.latest_version) + + def _handle_conflict_both_to_install(self, pkg1: str, pkg2: str, context: UpdateRequirementsContext): + for src_pkg in {p for p, data in context.pkgs_data.items() if + data['d'] and pkg1 in data['d'] or pkg2 in data['d']}: + if src_pkg not in context.cannot_upgrade: + reason = self.i18n['arch.update_summary.to_install.dep_conflict'].format("'{}'".format(pkg1), + "'{}'".format(pkg2)) + context.cannot_upgrade[src_pkg] = UpgradeRequirement(context.to_update[src_pkg], reason) + + del context.to_update[src_pkg] + + if src_pkg in context.repo_to_update: + del context.repo_to_update[src_pkg] + else: + del context.aur_to_update[src_pkg] + + del context.pkgs_data[src_pkg] + + for p in (pkg1, pkg2): + if p in context.to_install: + del context.to_install[p] + + if p in context.repo_to_install: + del context.repo_to_install[p] + else: + del context.aur_to_install[p] + + def _handle_conflict_to_update_and_to_install(self, pkg1: str, pkg2: str, pkg1_to_install: bool, context: UpdateRequirementsContext): + to_install, to_update = (pkg1, pkg2) if pkg1_to_install else (pkg2, pkg1) + to_install_srcs = {p for p, data in context.pkgs_data.items() if data['d'] and to_install in data['d']} + + if to_update not in context.cannot_upgrade: + srcs_str = ', '.join(("'{}'".format(p) for p in to_install_srcs)) + reason = self.i18n['arch.update_summary.to_update.conflicts_dep'].format("'{}'".format(to_install), + srcs_str) + context.cannot_upgrade[to_install] = UpgradeRequirement(context.to_update[to_update], reason) + + if to_update in context.to_update: + del context.to_update[to_update] + + for src_pkg in to_install_srcs: + src_to_install = src_pkg in context.to_install + pkg = context.to_install[src_pkg] if src_to_install else context.to_update[src_pkg] + if src_pkg not in context.cannot_upgrade: + reason = self.i18n['arch.update_summary.to_update.dep_conflicts'].format("'{}'".format(to_install), + "'{}'".format(to_update)) + context.cannot_upgrade[src_pkg] = UpgradeRequirement(pkg, reason) + + if src_to_install: + del context.to_install[src_pkg] + + if src_pkg in context.repo_to_install: + del context.repo_to_install[src_pkg] + else: + del context.aur_to_install[src_pkg] + else: + del context.to_update[src_pkg] + + if src_pkg in context.repo_to_update: + del context.repo_to_update[src_pkg] + else: + del context.aur_to_update[src_pkg] + + del context.pkgs_data[src_pkg] + + if to_install in context.to_install: + del context.to_install[to_install] + + def _handle_conflict_both_to_update(self, pkg1: str, pkg2: str, context: UpdateRequirementsContext): + if pkg1 not in context.cannot_upgrade: + reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), pkg2) + context.cannot_upgrade[pkg1] = UpgradeRequirement(pkg=context.to_update[pkg1], reason=reason) + + if pkg2 not in context.cannot_upgrade: + reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), pkg1) + context.cannot_upgrade[pkg2] = UpgradeRequirement(pkg=context.to_update[pkg2], reason=reason) + + for p in (pkg1, pkg2): + if p in context.to_update: + del context.to_update[p] + + if p in context.repo_to_update: + del context.repo_to_update[p] + else: + del context.aur_to_update[p] + + def _filter_and_map_conflicts(self, context: UpdateRequirementsContext) -> Dict[str, str]: + root_conflict = {} + mutual_conflicts = {} + + for p, data in context.pkgs_data.items(): + if data['c']: + for c in data['c']: + if c and c in context.installed_names: + # source = provided_map[c] + root_conflict[c] = p + + if (p, c) in root_conflict.items(): + mutual_conflicts[c] = p + + if mutual_conflicts: + for pkg1, pkg2 in mutual_conflicts.items(): + pkg1_to_install = pkg1 in context.to_install + pkg2_to_install = pkg2 in context.to_install + + if pkg1_to_install and pkg2_to_install: # remove both from to install and mark their source packages as 'cannot_update' + self._handle_conflict_both_to_install(pkg1, pkg2, context) + elif (pkg1_to_install and not pkg2_to_install) or (not pkg1_to_install and pkg2_to_install): + self._handle_conflict_to_update_and_to_install(pkg1, pkg2, pkg1_to_install, context) + else: + self._handle_conflict_both_to_update(pkg1, pkg2, context) # adding both to the 'cannot update' list + + for pkg1, pkg2 in mutual_conflicts.items(): # removing conflicting packages from the packages selected to update + for p in (pkg1, pkg2): + for c in context.pkgs_data[p]['c']: + # source = provided_map[c] + if c in root_conflict: + del root_conflict[c] + + if p in context.pkgs_data: + del context.pkgs_data[p] + + return root_conflict + + def _fill_conflicts(self, context: UpdateRequirementsContext, blacklist: Iterable[str] = None): + self.logger.info("Checking conflicts") + + root_conflict = self._filter_and_map_conflicts(context) + + sub_conflict = pacman.get_dependencies_to_remove(root_conflict.keys(), context.root_password) if root_conflict else None + + to_remove_map = {} + if sub_conflict: + for dep, source in sub_conflict.items(): + if dep not in to_remove_map and (not blacklist or dep not in blacklist): + req = ArchPackage(name=dep, installed=True, i18n=self.i18n) + to_remove_map[dep] = req + reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source) + context.to_remove[dep] = UpgradeRequirement(req, reason) + + if root_conflict: + for dep, source in root_conflict.items(): + if dep not in to_remove_map and (not blacklist or dep not in blacklist): + req = ArchPackage(name=dep, installed=True, i18n=self.i18n) + to_remove_map[dep] = req + reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), source) + context.to_remove[dep] = UpgradeRequirement(req, reason) + + if to_remove_map: + for name in to_remove_map.keys(): # upgrading lists + if name in context.pkgs_data: + del context.pkgs_data[name] + + if name in context.aur_to_update: + del context.aur_to_update[name] + + if name in context.repo_to_update: + del context.repo_to_update[name] + + removed_size = pacman.get_installed_size([*to_remove_map.keys()]) + + if removed_size: + for name, size in removed_size.items(): + if size is not None: + req = context.to_remove.get(name) + if req: + req.extra_size = size + + def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict): + version = None + + if pkg_data[1] == 'arch': + try: + info = self.aur_client.get_src_info(pkg_data[0]) + + if info: + version = info.get('pkgver') + + if not version: + self.logger.warning("No version declared in SRCINFO of '{}'".format(pkg_data[0])) + else: + self.logger.warning("Could not retrieve the SRCINFO for '{}'".format(pkg_data[0])) + except: + self.logger.warning("Could not retrieve the SRCINFO for '{}'".format(pkg_data[0])) + else: + version = pacman.get_version_for_not_installed(pkg_data[0]) + + output[idx] = ArchPackage(name=pkg_data[0], version=version, latest_version=version, repository=pkg_data[1], i18n=self.i18n) + + def _fill_to_install(self, context: UpdateRequirementsContext) -> bool: + ti = time.time() + self.logger.info("Discovering updates missing packages") + deps_data, deps_checked = {}, set() + deps = self.deps_analyser.map_missing_deps(pkgs_data=context.pkgs_data, + provided_map=context.provided_map, + aur_index=context.aur_index, + deps_checked=deps_checked, + sort=True, + deps_data=deps_data, + remote_provided_map=context.remote_provided_map, + remote_repo_map=context.remote_repo_map, + watcher=self.watcher) + + if deps is None: + tf = time.time() + self.logger.info("It took {0:.2f} seconds to retrieve required upgrade packages".format(tf - ti)) + return False # the user called the process off + + if deps: # filtering selected packages + selected_names = {p for p in context.to_update} + deps = [dep for dep in deps if dep[0] not in selected_names] + + if deps: + sorted_pkgs = {} + aur_to_install_data = {} + all_to_install_data = {} + + for idx, dep in enumerate(deps): + data = deps_data[dep[0]] + pkg = ArchPackage(name=dep[0], version=data['v'], latest_version=data['v'], repository=dep[1], i18n=self.i18n) + sorted_pkgs[idx] = pkg + context.to_install[dep[0]] = pkg + + if pkg.repository == 'arch': + context.aur_to_install[pkg.name] = pkg + aur_to_install_data[pkg.name] = data + else: + context.repo_to_install[pkg.name] = pkg + + if context.repo_to_install: + all_to_install_data.update(pacman.map_updates_data(context.repo_to_install.keys())) + + if aur_to_install_data: + all_to_install_data.update(aur_to_install_data) + + if all_to_install_data: + context.pkgs_data.update(all_to_install_data) + self._fill_conflicts(context, context.to_remove.keys()) + + tf = time.time() + self.logger.info("It took {0:.2f} seconds to retrieve required upgrade packages".format(tf - ti)) + return True + + def __fill_provided_map(self, context: UpdateRequirementsContext): + ti = time.time() + self.logger.info("Filling provided names") + context.installed_names = pacman.list_installed_names() + installed_to_ignore = set() + + for pkgname in context.to_update: + pacman.fill_provided_map(pkgname, pkgname, context.provided_map) + installed_to_ignore.add(pkgname) + + pdata = context.pkgs_data.get(pkgname) + if pdata and pdata['p']: + pacman.fill_provided_map('{}={}'.format(pkgname, pdata['v']), pkgname, context.provided_map) + for p in pdata['p']: + pacman.fill_provided_map(p, pkgname, context.provided_map) + split_provided = p.split('=') + + if len(split_provided) > 1 and split_provided[0] != p: + pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map) + + if installed_to_ignore: # filling the provided names of the installed + installed_to_query = context.installed_names.difference(installed_to_ignore) + + if installed_to_query: + context.provided_map.update(pacman.map_provided(remote=False, pkgs=installed_to_query)) + + tf = time.time() + self.logger.info("Filling provided names took {0:.2f} seconds".format(tf - ti)) + + def __fill_aur_index(self, context: UpdateRequirementsContext): + if context.arch_config['arch']: + self.logger.info("Loading AUR index") + names = self.aur_client.read_index() + + if names: + context.aur_index.update(names) + self.logger.info("AUR index loaded on the context") + + def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None) -> UpgradeRequirement: + requirement = UpgradeRequirement(pkg) + + if pkg.repository != 'arch': + data = context.pkgs_data[pkg.name] + requirement.required_size = data['ds'] + requirement.extra_size = data['s'] + + current_size = installed_sizes.get(pkg.name) if installed_sizes else None + + if current_size is not None and data['s']: + requirement.extra_size = data['s'] - current_size + + return requirement + + def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: dict) -> UpgradeRequirements: + res = UpgradeRequirements([], [], [], []) + + remote_provided_map = pacman.map_provided(remote=True) + remote_repo_map = pacman.map_repositories() + context = UpdateRequirementsContext(to_update={}, repo_to_update={}, aur_to_update={}, repo_to_install={}, + aur_to_install={}, to_install={}, pkgs_data={}, cannot_upgrade={}, + to_remove={}, installed_names=set(), provided_map={}, aur_index=set(), + arch_config=arch_config, root_password=root_password, + remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map) + self.__fill_aur_index(context) + + aur_data = {} + aur_srcinfo_threads = [] + for p in pkgs: + context.to_update[p.name] = p + if p.repository == 'arch': + context.aur_to_update[p.name] = p + t = Thread(target=self._fill_aur_pkg_update_data, args=(p, aur_data), daemon=True) + t.start() + aur_srcinfo_threads.append(t) + else: + context.repo_to_update[p.name] = p + + if context.aur_to_update: + for t in aur_srcinfo_threads: + t.join() + + self.logger.info("Filling updates data") + + if context.repo_to_update: + context.pkgs_data.update(pacman.map_updates_data(context.repo_to_update.keys())) + + if aur_data: + context.pkgs_data.update(aur_data) + + self.__fill_provided_map(context) + + if context.pkgs_data: + self._fill_conflicts(context) + + try: + if not self._fill_to_install(context): + self.logger.info("The operation was cancelled by the user") + return + except PackageNotFoundException as e: + self.logger.error("Package '{}' not found".format(e.name)) + return + + if context.to_update: + installed_sizes = pacman.get_installed_size(list(context.to_update.keys())) + + sorted_pkgs = [] + + if context.repo_to_update: # only sorting by name ( pacman already knows the best order to perform the upgrade ) + sorted_pkgs.extend(context.repo_to_update.values()) + sorted_pkgs.sort(key=lambda pkg: pkg.name) + + if context.aur_to_update: # adding AUR packages in the end + sorted_aur = sorting.sort(context.aur_to_update.keys(), context.pkgs_data, context.provided_map) + + for aur_pkg in sorted_aur: + sorted_pkgs.append(context.aur_to_update[aur_pkg[0]]) + + res.to_upgrade = [self._map_requirement(pkg, context, installed_sizes) for pkg in sorted_pkgs] + + if context.to_remove: + res.to_remove = [p for p in context.to_remove.values()] + + if context.cannot_upgrade: + res.cannot_upgrade = [d for d in context.cannot_upgrade.values()] + + if context.to_install: + res.to_install = [self._map_requirement(p, context) for p in context.to_install.values()] + + return res diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 029ac9a1..f9011ff3 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -2,18 +2,22 @@ import logging import os import re import time -from multiprocessing import Process +import traceback from pathlib import Path from threading import Thread import requests from bauh.api.abstract.context import ApplicationContext -from bauh.commons.system import run_cmd +from bauh.api.abstract.handler import TaskManager +from bauh.commons.html import bold +from bauh.commons.system import run_cmd, new_root_subprocess, ProcessHandler from bauh.gems.arch import pacman, disk, CUSTOM_MAKEPKG_FILE, CONFIG_DIR, BUILD_DIR, \ - AUR_INDEX_FILE, config + AUR_INDEX_FILE, get_icon_path, database, mirrors +from bauh.gems.arch.aur import URL_INDEX +from bauh.gems.arch.model import ArchPackage +from bauh.view.util.translation import I18n -URL_INDEX = 'https://aur.archlinux.org/packages.gz' URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&arg={}' GLOBAL_MAKEPKG = '/etc/makepkg.conf' @@ -27,6 +31,7 @@ class AURIndexUpdater(Thread): def __init__(self, context: ApplicationContext): super(AURIndexUpdater, self).__init__(daemon=True) self.http_client = context.http_client + self.i18n = context.i18n self.logger = context.logger def run(self): @@ -50,39 +55,102 @@ class AURIndexUpdater(Thread): except requests.exceptions.ConnectionError: self.logger.warning('No internet connection: could not pre-index packages') + self.logger.info("Finished") -class ArchDiskCacheUpdater(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else Process): - def __init__(self, logger: logging.Logger, disk_cache: bool): +class ArchDiskCacheUpdater(Thread): + + def __init__(self, task_man: TaskManager, arch_config: dict, i18n: I18n, logger: logging.Logger): super(ArchDiskCacheUpdater, self).__init__(daemon=True) self.logger = logger - self.disk_cache = disk_cache + self.task_man = task_man + self.task_id = 'arch_cache_up' + self.i18n = i18n + self.prepared = 0 + self.prepared_template = self.i18n['arch.task.disk_cache.prepared'] + ': {}/ {}' + self.indexed = 0 + self.indexed_template = self.i18n['arch.task.disk_cache.indexed'] + ': {}/ {}' + self.to_index = 0 + self.progress = 0 # progress is defined by the number of packages prepared and indexed + self.repositories = arch_config['repositories'] + self.aur = arch_config['arch'] + + def update_prepared(self, pkgname: str, add: bool = True): + if add: + self.prepared += 1 + + sub = self.prepared_template.format(self.prepared, self.to_index) + progress = ((self.prepared + self.indexed) / self.progress) * 100 if self.progress > 0 else 0 + self.task_man.update_progress(self.task_id, progress, sub) + + def update_indexed(self, pkgname: str): + self.indexed += 1 + sub = self.indexed_template.format(self.indexed, self.to_index) + progress = ((self.prepared + self.indexed) / self.progress) * 100 if self.progress > 0 else 0 + self.task_man.update_progress(self.task_id, progress, sub) def run(self): - if self.disk_cache: - self.logger.info('Pre-caching installed AUR packages data to disk') - installed = pacman.list_and_map_installed() + if not any([self.aur, self.repositories]): + return - saved = 0 - if installed and installed['not_signed']: - saved = disk.save_several({app for app in installed['not_signed']}, 'aur', overwrite=False) + ti = time.time() + self.task_man.register_task(self.task_id, self.i18n['arch.task.disk_cache'], get_icon_path()) - self.logger.info('Pre-cached data of {} AUR packages to the disk'.format(saved)) + self.logger.info('Pre-caching installed Arch packages data to disk') + installed = pacman.map_installed(repositories=self.repositories, aur=self.aur) + + self.task_man.update_progress(self.task_id, 0, self.i18n['arch.task.disk_cache.reading']) + for k in ('signed', 'not_signed'): + installed[k] = {p for p in installed[k] if not os.path.exists(ArchPackage.disk_cache_path(p))} + + saved = 0 + pkgs = {*installed['signed'], *installed['not_signed']} + + repo_map = {} + + if installed['not_signed']: + repo_map.update({p: 'arch' for p in installed['not_signed']}) + + if installed['signed']: + repo_map.update(pacman.map_repositories(installed['signed'])) + + self.to_index = len(pkgs) + self.progress = self.to_index * 2 + self.update_prepared(None, add=False) + + saved += disk.save_several(pkgs, repo_map, when_prepared=self.update_prepared, after_written=self.update_indexed) + self.task_man.update_progress(self.task_id, 100, None) + self.task_man.finish_task(self.task_id) + + tf = time.time() + time_msg = 'Took {0:.2f} seconds'.format(tf - ti) + self.logger.info('Pre-cached data of {} Arch packages to the disk. {}'.format(saved, time_msg)) class ArchCompilationOptimizer(Thread): - def __init__(self, logger: logging.Logger): + def __init__(self, arch_config: dict, i18n: I18n, logger: logging.Logger, task_man: TaskManager = None): super(ArchCompilationOptimizer, self).__init__(daemon=True) self.logger = logger + self.i18n = i18n self.re_compress_xz = re.compile(r'#?\s*COMPRESSXZ\s*=\s*.+') self.re_compress_zst = re.compile(r'#?\s*COMPRESSZST\s*=\s*.+') self.re_build_env = re.compile(r'\s+BUILDENV\s*=.+') self.re_ccache = re.compile(r'!?ccache') + self.task_man = task_man + self.task_id = 'arch_make_optm' + self.optimizations = bool(arch_config['optimize']) def _is_ccache_installed(self) -> bool: return bool(run_cmd('which ccache', print_error=False)) + def _update_progress(self, progress: float, substatus: str = None): + if self.task_man: + self.task_man.update_progress(self.task_id, progress, substatus) + + if progress == 100: + self.task_man.finish_task(self.task_id) + def optimize(self): ti = time.time() try: @@ -115,6 +183,8 @@ class ArchCompilationOptimizer(Thread): else: optimizations.append('MAKEFLAGS="-j$(nproc)"') + self._update_progress(20) + compress_xz = self.re_compress_xz.findall(custom_makepkg or global_makepkg) if compress_xz: @@ -128,6 +198,8 @@ class ArchCompilationOptimizer(Thread): else: optimizations.append('COMPRESSXZ=(xz -c -z - --threads=0)') + self._update_progress(40) + compress_zst = self.re_compress_zst.findall(custom_makepkg or global_makepkg) if compress_zst: @@ -141,6 +213,8 @@ class ArchCompilationOptimizer(Thread): else: optimizations.append('COMPRESSZST=(zstd -c -z -q - --threads=0)') + self._update_progress(60) + build_envs = self.re_build_env.findall(custom_makepkg or global_makepkg) if build_envs: @@ -169,6 +243,8 @@ class ArchCompilationOptimizer(Thread): self.logger.info('Adding a BUILDENV declaration') optimizations.append('BUILDENV=(ccache)') + self._update_progress(80) + if optimizations: generated_by = '# \n' custom_makepkg = custom_makepkg + '\n' + generated_by + '\n'.join(optimizations) + '\n' @@ -185,13 +261,12 @@ class ArchCompilationOptimizer(Thread): os.remove(CUSTOM_MAKEPKG_FILE) tf = time.time() + self._update_progress(100) self.logger.info("Optimizations took {0:.2f} seconds".format(tf - ti)) self.logger.info('Finished') def run(self): - local_config = config.read_config(update_file=True) - - if not local_config['optimize']: + if not self.optimizations: self.logger.info("Arch packages compilation optimizations are disabled") if os.path.exists(CUSTOM_MAKEPKG_FILE): @@ -200,4 +275,113 @@ class ArchCompilationOptimizer(Thread): self.logger.info('Finished') else: - self.optimize() + if self.task_man: + self.task_man.register_task(self.task_id, self.i18n['arch.task.optimizing'].format(bold('makepkg.conf')), get_icon_path()) + + self.optimize() + + +class RefreshMirrors(Thread): + + def __init__(self, taskman: TaskManager, root_password: str, i18n: I18n, sort_limit: int, logger: logging.Logger): + super(RefreshMirrors, self).__init__(daemon=True) + self.taskman = taskman + self.i18n = i18n + self.logger = logger + self.root_password = root_password + self.task_id = "arch_mirrors" + self.sort_limit = sort_limit + + def run(self): + self.logger.info("Refreshing mirrors") + self.taskman.register_task(self.task_id, self.i18n['arch.task.mirrors'], get_icon_path()) + + handler = ProcessHandler() + try: + success, output = handler.handle_simple(pacman.refresh_mirrors(self.root_password)) + + if success: + + if self.sort_limit is not None and self.sort_limit >= 0: + self.taskman.update_progress(self.task_id, 50, self.i18n['arch.custom_action.refresh_mirrors.status.updating']) + try: + handler.handle_simple(pacman.sort_fastest_mirrors(self.root_password, self.sort_limit)) + except: + self.logger.error("Could not sort mirrors by speed") + traceback.print_exc() + + mirrors.register_sync(self.logger) + else: + self.logger.error("It was not possible to refresh mirrors") + except: + self.logger.error("It was not possible to refresh mirrors") + traceback.print_exc() + + self.taskman.update_progress(self.task_id, 100, None) + self.taskman.finish_task(self.task_id) + self.logger.info("Finished") + + +class SyncDatabases(Thread): + + def __init__(self, taskman: TaskManager, root_password: str, i18n: I18n, logger: logging.Logger, refresh_mirrors: RefreshMirrors = None): + super(SyncDatabases, self).__init__(daemon=True) + self.task_man = taskman + self.i18n = i18n + self.taskman = taskman + self.task_id = "arch_dbsync" + self.root_password = root_password + self.refresh_mirrors = refresh_mirrors + self.logger = logger + + def run(self) -> None: + self.logger.info("Synchronizing databases") + self.taskman.register_task(self.task_id, self.i18n['arch.sync_databases.substatus'], get_icon_path()) + + if self.refresh_mirrors and self.refresh_mirrors.is_alive(): + self.taskman.update_progress(self.task_id, 0, self.i18n['arch.task.sync_databases.waiting']) + self.refresh_mirrors.join() + + progress = 10 + dbs = pacman.get_databases() + self.taskman.update_progress(self.task_id, progress, None) + + if dbs: + inc = 90 / len(dbs) + try: + p = new_root_subprocess(['pacman', '-Syy'], self.root_password) + + dbs_read, last_db = 0, None + + for o in p.stdout: + line = o.decode().strip() + + if line and line.startswith('downloading'): + db = line.split(' ')[1].strip() + + if last_db is None or last_db != db: + last_db = db + dbs_read += 1 + progress = dbs_read * inc + else: + progress += 0.25 + + self.taskman.update_progress(self.task_id, progress, self.i18n['arch.task.sync_sb.status'].format(db)) + + for o in p.stderr: + o.decode() + + p.wait() + + if p.returncode == 0: + database.register_sync(self.logger) + else: + self.logger.error("Could not synchronize database") + + except: + self.logger.info("Error while synchronizing databases") + traceback.print_exc() + + self.taskman.update_progress(self.task_id, 100, None) + self.taskman.finish_task(self.task_id) + self.logger.info("Finished") diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index c8927c6a..1a3e73da 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -4,17 +4,18 @@ from math import floor from threading import Thread from typing import List, Set, Type, Tuple -from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext +from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \ + UpgradeRequirement from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion, \ - SuggestionPriority + SuggestionPriority, PackageStatus from bauh.api.abstract.view import MessageType, FormComponent, SingleSelectComponent, InputOption, SelectViewType, \ ViewComponent, PanelComponent from bauh.commons import user from bauh.commons.config import save_config from bauh.commons.html import strip_html, bold -from bauh.commons.system import SystemProcess, ProcessHandler, SimpleProcess +from bauh.commons.system import SystemProcess, ProcessHandler from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE from bauh.gems.flatpak.config import read_config from bauh.gems.flatpak.constants import FLATHUB_API_URL @@ -59,6 +60,7 @@ class FlatpakManager(SoftwareManager): else: app.fill_cached_data(api_data) + app.status = PackageStatus.READY return app @@ -184,18 +186,35 @@ class FlatpakManager(SoftwareManager): super(FlatpakManager, self).clean_cache_for(pkg) self.api_cache.delete(pkg.id) - def update(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: - related, deps = False, False - ref = pkg.ref + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: + flatpak_version = flatpak.get_version() + for req in requirements.to_upgrade: + watcher.change_status("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], req.pkg.name, req.pkg.version)) + related, deps = False, False + ref = req.pkg.ref - if pkg.partial and flatpak.get_version() < '1.5': - related, deps = True, True - ref = pkg.base_ref + if req.pkg.partial and flatpak_version < '1.5': + related, deps = True, True + ref = req.pkg.base_ref - return ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(app_ref=ref, - installation=pkg.installation, - related=related, - deps=deps))) + try: + res = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(app_ref=ref, + installation=req.pkg.installation, + related=related, + deps=deps))) + + watcher.change_substatus('') + if not res: + self.logger.warning("Could not upgrade '{}'".format(req.pkg.id)) + return False + except: + watcher.change_substatus('') + self.logger.error("An error occurred while upgrading '{}'".format(req.pkg.id)) + traceback.print_exc() + return False + + watcher.change_substatus('') + return True def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref, pkg.installation))) @@ -337,8 +356,8 @@ class FlatpakManager(SoftwareManager): def requires_root(self, action: str, pkg: FlatpakApplication): return action == 'downgrade' and pkg.installation == 'system' - def prepare(self): - Thread(target=read_config, daemon=True).start() + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): + Thread(target=read_config, args=(True,), daemon=True).start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: updates = [] @@ -360,7 +379,8 @@ class FlatpakManager(SoftwareManager): for app in to_update: updates.append(PackageUpdate(pkg_id='{}:{}:{}'.format(app.id, app.branch, app.installation), - pkg_type='flatpak', + pkg_type='Flatpak', + name=app.name, version=app.version)) return updates @@ -447,7 +467,7 @@ class FlatpakManager(SoftwareManager): InputOption(label=self.i18n['flatpak.config.install_level.user'].capitalize(), value='user', tooltip=self.i18n['flatpak.config.install_level.user.tip']), - InputOption(label=self.i18n['flatpak.config.install_level.ask'].capitalize(), + InputOption(label=self.i18n['ask'].capitalize(), value=None, tooltip=self.i18n['flatpak.config.install_level.ask.tip'].format(app=self.context.app_name))] fields.append(SingleSelectComponent(label=self.i18n['flatpak.config.install_level'], @@ -469,6 +489,28 @@ class FlatpakManager(SoftwareManager): except: return False, [traceback.format_exc()] + def get_upgrade_requirements(self, pkgs: List[FlatpakApplication], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements: + flatpak_version = flatpak.get_version() + + user_pkgs, system_pkgs = [], [] + + for pkg in pkgs: + if pkg.installation == 'user': + user_pkgs.append(pkg) + else: + system_pkgs.append(pkg) + + for apps_by_install in ((user_pkgs, 'user'), (system_pkgs, 'system')): + if apps_by_install[0]: + sizes = flatpak.map_update_download_size([str(p.id) for p in apps_by_install[0]], apps_by_install[1], flatpak_version) + + if sizes: + for p in apps_by_install[0]: + p.size = sizes.get(str(p.id)) + + to_update = [UpgradeRequirement(pkg=p, extra_size=p.size, required_size=p.size) for p in self.sort_update_order(pkgs)] + return UpgradeRequirements(None, None, to_update, []) + def sort_update_order(self, pkgs: List[FlatpakApplication]) -> List[FlatpakApplication]: partials, runtimes, apps = [], [], [] diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index c9401f7a..56eda91d 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -2,10 +2,11 @@ import re import subprocess import traceback from datetime import datetime -from typing import List, Dict, Set +from typing import List, Dict, Set, Iterable from bauh.api.exception import NoInternetException from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess, ProcessHandler +from bauh.commons.util import size_to_byte RE_SEVERAL_SPACES = re.compile(r'\s+') @@ -385,3 +386,28 @@ def list_remotes() -> Dict[str, Set[str]]: def run(app_id: str): subprocess.Popen(['flatpak', 'run', app_id]) + + +def map_update_download_size(app_ids: Iterable[str], installation: str, version: str) -> Dict[str, int]: + success, output = ProcessHandler().handle_simple(SimpleProcess(['flatpak', 'update', '--{}'.format(installation)])) + if version >= '1.5': + res = {} + p = re.compile(r'^\d+.\t') + p2 = re.compile(r'\s([0-9.?a-zA-Z]+)\s?') + for l in output.split('\n'): + if l: + line = l.strip() + + if line: + found = p.match(line) + + if found: + line_split = line.split('\t') + line_id = line_split[2].strip() + + related_id = [appid for appid in app_ids if appid == line_id] + + if related_id: + size = p2.findall(line_split[6])[0].split('?') + res[related_id[0].strip()] = size_to_byte(float(size[0]), size[1]) + return res diff --git a/bauh/gems/flatpak/model.py b/bauh/gems/flatpak/model.py index 38f17508..ca9129d9 100644 --- a/bauh/gems/flatpak/model.py +++ b/bauh/gems/flatpak/model.py @@ -1,6 +1,4 @@ -import copy - -from bauh.api.abstract.model import SoftwarePackage +from bauh.api.abstract.model import SoftwarePackage, PackageStatus from bauh.commons import resource from bauh.gems.flatpak import ROOT_DIR from bauh.view.util.translation import I18n @@ -50,7 +48,7 @@ class FlatpakApplication(SoftwarePackage): return self.get_default_icon_path() def is_application(self): - return not self.runtime + return not self.runtime and not self.partial def get_disk_cache_path(self): return super(FlatpakApplication, self).get_disk_cache_path() + '/installed/' + self.id @@ -77,15 +75,27 @@ class FlatpakApplication(SoftwarePackage): return self.origin def gen_partial(self, partial_id: str) -> "FlatpakApplication": - partial = copy.deepcopy(self) + partial = FlatpakApplication() + partial.partial = True partial.id = partial_id partial.base_id = self.id + partial.installation = self.installation + partial.origin = self.origin + partial.branch = self.branch + partial.i18n = self.i18n + partial.arch = self.arch + partial.name = self.name + partial.version = self.version + partial.latest_version = self.latest_version + partial.installed = self.installed + partial.runtime = True if self.ref: partial.base_ref = self.ref partial.ref = '/'.join((partial_id, *self.ref.split('/')[1:])) + partial.status = PackageStatus.READY + partial.name += ' ( {} )'.format(partial_id.split('.')[-1]) - partial.partial = True return partial def get_name_tooltip(self) -> str: @@ -93,3 +103,6 @@ class FlatpakApplication(SoftwarePackage): return '{} ( {} )'.format(self.name, self.i18n[self.installation.lower()]) return self.name + + def supports_backup(self) -> bool: + return True diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index 3a61c697..89eec7dd 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -1,55 +1,54 @@ -gem.flatpak.info=Aplicacions disponibles a Flathub i altres repositoris configurats al vostre sistema -flatpak.info.arch=arquitectura -flatpak.info.branch=branca -flatpak.info.collection=col·lecció -flatpak.info.commit=revisió -flatpak.info.date=data -flatpak.info.description=descripció -flatpak.info.id=id -flatpak.info.installation=instal·lació -flatpak.info.installation.user=usuari -flatpak.info.installation.system=sistema -flatpak.info.installed=instal·lat -flatpak.info.license=llicència -flatpak.info.name=nom -flatpak.info.origin=origen -flatpak.info.parent=pare -flatpak.info.ref=ref -flatpak.info.runtime=entorn d’execució -flatpak.info.sdk=sdk -flatpak.info.subject=tema -flatpak.info.type=tipus -flatpak.info.version=versió -flatpak.notification.no_remotes=No hi ha dipòsits («remotes») del Flatpak configurats. No podreu cercar aplicacions Flatpak. -flatpak.notification.disable=Si no voleu utilitzar aplicacions Flatpak, desmarqueu {} a {} -flatpak.downgrade.impossible.title=Error -flatpak.downgrade.impossible.body=No s’ha pogut revertir la versió: l’aplicació és a la seva primera versió -flatpak.history.date=data -flatpak.history.commit=revisió -flatpak.history.subject=tema -flatpak.downgrade.commits=S’estan llegint les revisions del paquet -flatpak.downgrade.reverting=S’està revertint la versió -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=informe d’errors -flatpak.info.currentreleasedate=data de publicació -flatpak.info.currentreleasedescription=descripció de la versió -flatpak.info.currentreleaseversion=versió -flatpak.info.donationurl=donatius -flatpak.info.downloadflatpakrefurl=ref -flatpak.info.helpurl=ajuda -flatpak.info.homepageurl=pàgina -flatpak.info.instoresincedate=afegit el -flatpak.info.projectlicense=llicència -flatpak.info.translateurl=Traducció -flatpak.info.developername=desenvolupador -flatpak.install.install_level.title=Tipus d'instal·lació -flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu ( sistema ) ? -flatpak.install.bad_install_level.body=Valor invàlid per a {field} al fitxer de configuració {file} -flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote ) +flatpak.config.install_level=level +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation flatpak.config.install_level.system=system flatpak.config.install_level.system.tip=Applications will be installed for all the device users flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user -flatpak.config.install_level.ask=ask -flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation -flatpak.config.install_level=level \ No newline at end of file +flatpak.downgrade.commits=S’estan llegint les revisions del paquet +flatpak.downgrade.impossible.body=No s’ha pogut revertir la versió: l’aplicació és a la seva primera versió +flatpak.downgrade.impossible.title=Error +flatpak.downgrade.reverting=S’està revertint la versió +flatpak.history.commit=revisió +flatpak.history.date=data +flatpak.history.subject=tema +flatpak.info.arch=arquitectura +flatpak.info.branch=branca +flatpak.info.bugtrackerurl=informe d’errors +flatpak.info.collection=col·lecció +flatpak.info.commit=revisió +flatpak.info.currentreleasedate=data de publicació +flatpak.info.currentreleasedescription=descripció de la versió +flatpak.info.currentreleaseversion=versió +flatpak.info.date=data +flatpak.info.description=descripció +flatpak.info.developername=desenvolupador +flatpak.info.donationurl=donatius +flatpak.info.downloadflatpakrefurl=ref +flatpak.info.flatpakappid=id +flatpak.info.helpurl=ajuda +flatpak.info.homepageurl=pàgina +flatpak.info.id=id +flatpak.info.installation=instal·lació +flatpak.info.installation.system=sistema +flatpak.info.installation.user=usuari +flatpak.info.installed=instal·lat +flatpak.info.instoresincedate=afegit el +flatpak.info.license=llicència +flatpak.info.name=nom +flatpak.info.origin=origen +flatpak.info.parent=pare +flatpak.info.projectlicense=llicència +flatpak.info.ref=ref +flatpak.info.runtime=entorn d’execució +flatpak.info.sdk=sdk +flatpak.info.subject=tema +flatpak.info.translateurl=Traducció +flatpak.info.type=tipus +flatpak.info.version=versió +flatpak.install.bad_install_level.body=Valor invàlid per a {field} al fitxer de configuració {file} +flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu ( sistema ) ? +flatpak.install.install_level.title=Tipus d'instal·lació +flatpak.notification.disable=Si no voleu utilitzar aplicacions Flatpak, desmarqueu {} a {} +flatpak.notification.no_remotes=No hi ha dipòsits («remotes») del Flatpak configurats. No podreu cercar aplicacions Flatpak. +flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote ) +gem.flatpak.info=Aplicacions disponibles a Flathub i altres repositoris configurats al vostre sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index 707a44c0..6ead2323 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -1,54 +1,54 @@ -gem.flatpak.info=Verfügbare Anwendungen in den konfigurierten Repositories -flatpak.notification.no_remotes=Keine Flatpak Remotes sind konfiguriert. Die Suche nach Flatpak Apps ist nicht möglich. -flatpak.notification.disable=Um keine Flatpak Anwendungen zu verwenden deaktiviere {} unter {} -flatpak.downgrade.impossible.title=Fehler -flatpak.downgrade.impossible.body=Downgrade nicht möglich. Dies ist die erste Version der App -flatpak.downgrade.commits=Paket-Commits lesen -flatpak.downgrade.reverting=Version zurücksetzen -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=bug tracker URL -flatpak.info.currentreleasedate=Veröffentlichungsdatum -flatpak.info.currentreleasedescription=Versionsbeschreibung -flatpak.info.currentreleaseversion=Version -flatpak.info.donationurl=URL für Spenden -flatpak.info.downloadflatpakrefurl=Ref -flatpak.info.helpurl=URL für Hilfe -flatpak.info.homepageurl=Haupt-URL -flatpak.info.instoresincedate=Hinzugefügt -flatpak.info.projectlicense=Lizenz -flatpak.info.translateurl=Übersetzungs-URL -flatpak.info.developername=Entwickler -flatpak.info.arch=Arch -flatpak.info.branch=Branch -flatpak.info.collection=Sammlung -flatpak.info.commit=Commit -flatpak.info.date=Datum -flatpak.info.description=Beschreibung -flatpak.info.id=Id -flatpak.info.installation=Installation -flatpak.info.installation.user=Benutzer -flatpak.info.installation.system=System -flatpak.info.installed=installiert -flatpak.info.license=Lizenz -flatpak.info.name=Name -flatpak.info.origin=Herkunft -flatpak.info.parent=parent -flatpak.info.ref=ref -flatpak.info.runtime=runtime -flatpak.info.sdk=sdk -flatpak.info.subject=Thema -flatpak.info.type=Typ -flatpak.info.version=Version -flatpak.history.date=Datum -flatpak.history.commit=Commit -flatpak.install.install_level.title=Installationstyp -flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden ( system ) ? -flatpak.install.bad_install_level.body=Ungültiger Wert für {field} in der Konfigurationsdatei {file} -flatpak.remotes.system_flathub.error=Flathub konnte nicht als System-Repository ( remote ) hinzugefügt werden +flatpak.config.install_level=level +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation flatpak.config.install_level.system=system flatpak.config.install_level.system.tip=Applications will be installed for all the device users flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user -flatpak.config.install_level.ask=ask -flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation -flatpak.config.install_level=level \ No newline at end of file +flatpak.downgrade.commits=Paket-Commits lesen +flatpak.downgrade.impossible.body=Downgrade nicht möglich. Dies ist die erste Version der App +flatpak.downgrade.impossible.title=Fehler +flatpak.downgrade.reverting=Version zurücksetzen +flatpak.history.commit=Commit +flatpak.history.date=Datum +flatpak.history.subject=subject +flatpak.info.arch=Arch +flatpak.info.branch=Branch +flatpak.info.bugtrackerurl=bug tracker URL +flatpak.info.collection=Sammlung +flatpak.info.commit=Commit +flatpak.info.currentreleasedate=Veröffentlichungsdatum +flatpak.info.currentreleasedescription=Versionsbeschreibung +flatpak.info.currentreleaseversion=Version +flatpak.info.date=Datum +flatpak.info.description=Beschreibung +flatpak.info.developername=Entwickler +flatpak.info.donationurl=URL für Spenden +flatpak.info.downloadflatpakrefurl=Ref +flatpak.info.flatpakappid=id +flatpak.info.helpurl=URL für Hilfe +flatpak.info.homepageurl=Haupt-URL +flatpak.info.id=Id +flatpak.info.installation=Installation +flatpak.info.installation.system=System +flatpak.info.installation.user=Benutzer +flatpak.info.installed=installiert +flatpak.info.instoresincedate=Hinzugefügt +flatpak.info.license=Lizenz +flatpak.info.name=Name +flatpak.info.origin=Herkunft +flatpak.info.parent=parent +flatpak.info.projectlicense=Lizenz +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=Thema +flatpak.info.translateurl=Übersetzungs-URL +flatpak.info.type=Typ +flatpak.info.version=Version +flatpak.install.bad_install_level.body=Ungültiger Wert für {field} in der Konfigurationsdatei {file} +flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden ( system ) ? +flatpak.install.install_level.title=Installationstyp +flatpak.notification.disable=Um keine Flatpak Anwendungen zu verwenden deaktiviere {} unter {} +flatpak.notification.no_remotes=Keine Flatpak Remotes sind konfiguriert. Die Suche nach Flatpak Apps ist nicht möglich. +flatpak.remotes.system_flathub.error=Flathub konnte nicht als System-Repository ( remote ) hinzugefügt werden +gem.flatpak.info=Verfügbare Anwendungen in den konfigurierten Repositories \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index fb571bbd..10ae4ff9 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -1,54 +1,54 @@ -gem.flatpak.info=Applications available on Flathub and other repositories configured on your system -flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. -flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} -flatpak.downgrade.impossible.title=Error -flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version -flatpak.downgrade.commits=Reading package commits -flatpak.downgrade.reverting=Reverting version -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=bug tracker URL -flatpak.info.currentreleasedate=release date -flatpak.info.currentreleasedescription=version description -flatpak.info.currentreleaseversion=version -flatpak.info.donationurl=donation URL -flatpak.info.downloadflatpakrefurl=Ref -flatpak.info.helpurl=Help url -flatpak.info.homepageurl=Home url -flatpak.info.instoresincedate=added in -flatpak.info.projectlicense=license -flatpak.info.translateurl=translate url -flatpak.info.developername=developer -flatpak.info.arch=arch -flatpak.info.branch=branch -flatpak.info.collection=collection -flatpak.info.commit=commit -flatpak.info.date=date -flatpak.info.description=description -flatpak.info.id=id -flatpak.info.installation=installation -flatpak.info.installation.user=user -flatpak.info.installation.system=system -flatpak.info.installed=installed -flatpak.info.license=license -flatpak.info.name=name -flatpak.info.origin=origin -flatpak.info.parent=parent -flatpak.info.ref=ref -flatpak.info.runtime=runtime -flatpak.info.sdk=sdk -flatpak.info.subject=subject -flatpak.info.type=type -flatpak.info.version=version -flatpak.history.date=date -flatpak.history.commit=commit -flatpak.install.install_level.title=Installation type -flatpak.install.install_level.body=Should {} be installed for all the device users ( system ) ? -flatpak.install.bad_install_level.body=Invalid value for {field} in the configuration file {file} -flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote ) +flatpak.config.install_level=level +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation flatpak.config.install_level.system=system flatpak.config.install_level.system.tip=Applications will be installed for all the device users flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user -flatpak.config.install_level.ask=ask -flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation -flatpak.config.install_level=level \ No newline at end of file +flatpak.downgrade.commits=Reading package commits +flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version +flatpak.downgrade.impossible.title=Error +flatpak.downgrade.reverting=Reverting version +flatpak.history.commit=commit +flatpak.history.date=date +flatpak.history.subject=subject +flatpak.info.arch=arch +flatpak.info.branch=branch +flatpak.info.bugtrackerurl=bug tracker URL +flatpak.info.collection=collection +flatpak.info.commit=commit +flatpak.info.currentreleasedate=release date +flatpak.info.currentreleasedescription=version description +flatpak.info.currentreleaseversion=version +flatpak.info.date=date +flatpak.info.description=description +flatpak.info.developername=developer +flatpak.info.donationurl=donation URL +flatpak.info.downloadflatpakrefurl=Ref +flatpak.info.flatpakappid=id +flatpak.info.helpurl=Help url +flatpak.info.homepageurl=Home url +flatpak.info.id=id +flatpak.info.installation=installation +flatpak.info.installation.system=system +flatpak.info.installation.user=user +flatpak.info.installed=installed +flatpak.info.instoresincedate=added in +flatpak.info.license=license +flatpak.info.name=name +flatpak.info.origin=origin +flatpak.info.parent=parent +flatpak.info.projectlicense=license +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=subject +flatpak.info.translateurl=translate url +flatpak.info.type=type +flatpak.info.version=version +flatpak.install.bad_install_level.body=Invalid value for {field} in the configuration file {file} +flatpak.install.install_level.body=Should {} be installed for all the device users ( system ) ? +flatpak.install.install_level.title=Installation type +flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} +flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. +flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote ) +gem.flatpak.info=Applications available on Flathub and other repositories configured on your system \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index fd6cfce9..2c0fc09e 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -1,55 +1,54 @@ -gem.flatpak.info=Aplicaciones disponibles en Flathub y otros repositorios configurados en su sistema -flatpak.info.arch=arquitectura -flatpak.info.branch=rama -flatpak.info.collection=colección -flatpak.info.commit=commit -flatpak.info.date=fecha -flatpak.info.description=descripción -flatpak.info.id=id -flatpak.info.installation=instalación -flatpak.info.installation.user=usuario -flatpak.info.installation.system=sistema -flatpak.info.installed=instalado -flatpak.info.license=licencia -flatpak.info.name=nombre -flatpak.info.origin=origen -flatpak.info.parent=padre -flatpak.info.ref=ref -flatpak.info.runtime=runtime -flatpak.info.sdk=sdk -flatpak.info.subject=tema -flatpak.info.type=tipo -flatpak.info.version=versión -flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak. -flatpak.notification.disable=Si no desea usar aplicativos Flatpak, desmarque {} en {} -flatpak.downgrade.impossible.title=Error -flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión -flatpak.history.date=fecha -flatpak.history.commit=commit -flatpak.history.subject=tema -flatpak.downgrade.commits=Leyendo commits del paquete -flatpak.downgrade.reverting=Revirtiendo la versión -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=reporte de errores -flatpak.info.currentreleasedate=fecha de lanzamiento -flatpak.info.currentreleasedescription=descripción de la versión -flatpak.info.currentreleaseversion=versión -flatpak.info.donationurl=donaciones -flatpak.info.downloadflatpakrefurl=ref -flatpak.info.helpurl=ayuda -flatpak.info.homepageurl=página -flatpak.info.instoresincedate=agregado en -flatpak.info.projectlicense=licencia -flatpak.info.translateurl=Traducción -flatpak.info.developername=desarrollador -flatpak.install.install_level.title=Tipo de instalación -flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo ( sistema )? -flatpak.install.bad_install_level.body=Valor inválido para {field} en el archivo de configuración {file} -flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote ) +flatpak.config.install_level=nivel +flatpak.config.install_level.ask.tip={app} preguntará el nivel que debe aplicarse durante la instalación de la aplicación flatpak.config.install_level.system=sistema flatpak.config.install_level.system.tip=Se instalarán aplicaciones para todos los usuarios del dispositivo flatpak.config.install_level.user=usuario flatpak.config.install_level.user.tip=La aplicación se instalará solo para el usuario actual -flatpak.config.install_level.ask=preguntar -flatpak.config.install_level.ask.tip={app} preguntará el nivel que debe aplicarse durante la instalación de la aplicación -flatpak.config.install_level=nivel \ No newline at end of file +flatpak.downgrade.commits=Leyendo commits del paquete +flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión +flatpak.downgrade.impossible.title=Error +flatpak.downgrade.reverting=Revirtiendo la versión +flatpak.history.commit=commit +flatpak.history.date=fecha +flatpak.history.subject=tema +flatpak.info.arch=arquitectura +flatpak.info.branch=rama +flatpak.info.bugtrackerurl=reporte de errores +flatpak.info.collection=colección +flatpak.info.commit=commit +flatpak.info.currentreleasedate=fecha de lanzamiento +flatpak.info.currentreleasedescription=descripción de la versión +flatpak.info.currentreleaseversion=versión +flatpak.info.date=fecha +flatpak.info.description=descripción +flatpak.info.developername=desarrollador +flatpak.info.donationurl=donaciones +flatpak.info.downloadflatpakrefurl=ref +flatpak.info.flatpakappid=id +flatpak.info.helpurl=ayuda +flatpak.info.homepageurl=página +flatpak.info.id=id +flatpak.info.installation=instalación +flatpak.info.installation.system=sistema +flatpak.info.installation.user=usuario +flatpak.info.installed=instalado +flatpak.info.instoresincedate=agregado en +flatpak.info.license=licencia +flatpak.info.name=nombre +flatpak.info.origin=origen +flatpak.info.parent=padre +flatpak.info.projectlicense=licencia +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=tema +flatpak.info.translateurl=Traducción +flatpak.info.type=tipo +flatpak.info.version=versión +flatpak.install.bad_install_level.body=Valor inválido para {field} en el archivo de configuración {file} +flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo ( sistema )? +flatpak.install.install_level.title=Tipo de instalación +flatpak.notification.disable=Si no desea usar aplicativos Flatpak, desmarque {} en {} +flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak. +flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote ) +gem.flatpak.info=Aplicaciones disponibles en Flathub y otros repositorios configurados en su sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index c0351a93..ff3a5ec1 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -1,34 +1,54 @@ -gem.flatpak.info=Applicazioni disponibili su Flathub e altri repository configurati sul tuo sistema -flatpak.notification.no_remotes=Nessun set remoti Flatpak. Non sarà possibile cercare app Flatpak. -flatpak.notification.disable=Ise non si desidera utilizzare le applicazioni Flatpak, deselezionare {} in {} -flatpak.downgrade.impossible.title=Errore -flatpak.downgrade.impossible.body=Iimpossibile effettuare il downgrade: l'app è nella sua prima versione -flatpak.downgrade.commits=lettura commits del pacchetto -flatpak.downgrade.reverting=Ripristino versione -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=bug tracker URL -flatpak.info.currentreleasedate=data di rilascio -flatpak.info.currentreleasedescription=version description -flatpak.info.currentreleaseversion=versione -flatpak.info.donationurl=donazioni URL -flatpak.info.downloadflatpakrefurl=Rif -flatpak.info.helpurl=Aiuto url -flatpak.info.homepageurl=Home url -flatpak.info.instoresincedate=aggiunto in -flatpak.info.projectlicense=licenza -flatpak.info.translateurl=traduci url -flatpak.info.developername=sviluppatore -flatpak.info.installation=installazione -flatpak.info.installation.user=utente -flatpak.info.installation.system=sistema -flatpak.install.install_level.title=Tipo di installazione -flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo ( sistema ) ? -flatpak.install.bad_install_level.body=Valore non valido per {field} nel file di configurazione {file} -flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote ) +flatpak.config.install_level=level +flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation flatpak.config.install_level.system=system flatpak.config.install_level.system.tip=Applications will be installed for all the device users flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user -flatpak.config.install_level.ask=ask -flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation -flatpak.config.install_level=level \ No newline at end of file +flatpak.downgrade.commits=lettura commits del pacchetto +flatpak.downgrade.impossible.body=Iimpossibile effettuare il downgrade: l'app è nella sua prima versione +flatpak.downgrade.impossible.title=Errore +flatpak.downgrade.reverting=Ripristino versione +flatpak.history.commit=commit +flatpak.history.date=date +flatpak.history.subject=subject +flatpak.info.arch=arch +flatpak.info.branch=branch +flatpak.info.bugtrackerurl=bug tracker URL +flatpak.info.collection=collection +flatpak.info.commit=commit +flatpak.info.currentreleasedate=data di rilascio +flatpak.info.currentreleasedescription=version description +flatpak.info.currentreleaseversion=versione +flatpak.info.date=date +flatpak.info.description=description +flatpak.info.developername=sviluppatore +flatpak.info.donationurl=donazioni URL +flatpak.info.downloadflatpakrefurl=Rif +flatpak.info.flatpakappid=id +flatpak.info.helpurl=Aiuto url +flatpak.info.homepageurl=Home url +flatpak.info.id=id +flatpak.info.installation=installazione +flatpak.info.installation.system=sistema +flatpak.info.installation.user=utente +flatpak.info.installed=installed +flatpak.info.instoresincedate=aggiunto in +flatpak.info.license=license +flatpak.info.name=name +flatpak.info.origin=origin +flatpak.info.parent=parent +flatpak.info.projectlicense=licenza +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=subject +flatpak.info.translateurl=traduci url +flatpak.info.type=type +flatpak.info.version=version +flatpak.install.bad_install_level.body=Valore non valido per {field} nel file di configurazione {file} +flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo ( sistema ) ? +flatpak.install.install_level.title=Tipo di installazione +flatpak.notification.disable=Ise non si desidera utilizzare le applicazioni Flatpak, deselezionare {} in {} +flatpak.notification.no_remotes=Nessun set remoti Flatpak. Non sarà possibile cercare app Flatpak. +flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote ) +gem.flatpak.info=Applicazioni disponibili su Flathub e altri repository configurati sul tuo sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index 1d0b3e82..36b9dd68 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -1,55 +1,54 @@ -gem.flatpak.info=Aplicativos disponíveis no Flathub e outros repositórios configurados no seu sistema -flatpak.info.arch=arquitetura -flatpak.info.branch=ramo -flatpak.info.collection=coleção -flatpak.info.commit=commit -flatpak.info.date=data -flatpak.info.description=descrição -flatpak.info.id=id -flatpak.info.installation=instalação -flatpak.info.installation.user=usuário -flatpak.info.installation.system=sistema -flatpak.info.installed=instalado -flatpak.info.license=licença -flatpak.info.name=nome -flatpak.info.origin=origem -flatpak.info.parent=pai -flatpak.info.ref=ref -flatpak.info.runtime=runtime -flatpak.info.sdk=sdk -flatpak.info.subject=assunto -flatpak.info.type=tipo -flatpak.info.version=versão -flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak. -flatpak.notification.disable=Se não deseja usar aplicativos Flatpak, desmarque {} em {} -flatpak.downgrade.impossible.title=Erro -flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão -flatpak.history.date=data -flatpak.history.commit=commit -flatpak.history.subject=assunto -flatpak.downgrade.commits=Lendo os commits do pacote -flatpak.downgrade.reverting=Revertendo a versão -flatpak.info.flatpakappid=id -flatpak.info.bugtrackerurl=reporte de erros -flatpak.info.currentreleasedate=data de lançamento -flatpak.info.currentreleasedescription=descrição da versão -flatpak.info.currentreleaseversion=versão -flatpak.info.donationurl=doações -flatpak.info.downloadflatpakrefurl=ref -flatpak.info.helpurl=ajuda -flatpak.info.homepageurl=página -flatpak.info.instoresincedate=adicionado em -flatpak.info.projectlicense=licença -flatpak.info.translateurl=tradução -flatpak.info.developername=desenvolvedor -flatpak.install.install_level.title=Tipo de instalação -flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo ( sistema ) ? -flatpak.install.bad_install_level.body=Valor inválido para {field} no arquivo de configuração {file} -flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote ) +flatpak.config.install_level=nível +flatpak.config.install_level.ask.tip=O {app} perguntará qual o nível deverá ser aplicado durante a instalação do aplicativo flatpak.config.install_level.system=sistema flatpak.config.install_level.system.tip=Os aplicativos serão instalados para todos os usuários do dispositivo flatpak.config.install_level.user=usuário flatpak.config.install_level.user.tip=Os aplicativos serão instalados somente para o usuário atual -flatpak.config.install_level.ask=perguntar -flatpak.config.install_level.ask.tip=O {app} perguntará qual o nível deverá ser aplicado durante a instalação do aplicativo -flatpak.config.install_level=nível \ No newline at end of file +flatpak.downgrade.commits=Lendo os commits do pacote +flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão +flatpak.downgrade.impossible.title=Erro +flatpak.downgrade.reverting=Revertendo a versão +flatpak.history.commit=commit +flatpak.history.date=data +flatpak.history.subject=assunto +flatpak.info.arch=arquitetura +flatpak.info.branch=ramo +flatpak.info.bugtrackerurl=reporte de erros +flatpak.info.collection=coleção +flatpak.info.commit=commit +flatpak.info.currentreleasedate=data de lançamento +flatpak.info.currentreleasedescription=descrição da versão +flatpak.info.currentreleaseversion=versão +flatpak.info.date=data +flatpak.info.description=descrição +flatpak.info.developername=desenvolvedor +flatpak.info.donationurl=doações +flatpak.info.downloadflatpakrefurl=ref +flatpak.info.flatpakappid=id +flatpak.info.helpurl=ajuda +flatpak.info.homepageurl=página +flatpak.info.id=id +flatpak.info.installation=instalação +flatpak.info.installation.system=sistema +flatpak.info.installation.user=usuário +flatpak.info.installed=instalado +flatpak.info.instoresincedate=adicionado em +flatpak.info.license=licença +flatpak.info.name=nome +flatpak.info.origin=origem +flatpak.info.parent=pai +flatpak.info.projectlicense=licença +flatpak.info.ref=ref +flatpak.info.runtime=runtime +flatpak.info.sdk=sdk +flatpak.info.subject=assunto +flatpak.info.translateurl=tradução +flatpak.info.type=tipo +flatpak.info.version=versão +flatpak.install.bad_install_level.body=Valor inválido para {field} no arquivo de configuração {file} +flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo ( sistema ) ? +flatpak.install.install_level.title=Tipo de instalação +flatpak.notification.disable=Se não deseja usar aplicativos Flatpak, desmarque {} em {} +flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak. +flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote ) +gem.flatpak.info=Aplicativos disponíveis no Flathub e outros repositórios configurados no seu sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/ru b/bauh/gems/flatpak/resources/locale/ru index af6e1bb8..4005a082 100644 --- a/bauh/gems/flatpak/resources/locale/ru +++ b/bauh/gems/flatpak/resources/locale/ru @@ -1,54 +1,54 @@ -gem.flatpak.info=Приложения, доступные на Flathub и других репозиториях, настроенных в вашей системе -flatpak.notification.no_remotes=Поддержка Flatpak не установлена. Поиск приложений Flatpak будет невозможен. -flatpak.notification.disable=Если вы не хотите использовать приложения Flatpak, снимите флажок {} в {} -flatpak.downgrade.impossible.title=Ошибка -flatpak.downgrade.impossible.body=Невозможно понизить версию: приложение находится в первой версии -flatpak.downgrade.commits=Чтение коммитов пакета -flatpak.downgrade.reverting=Возврат версии -flatpak.info.flatpakappid=Идентификатор -flatpak.info.bugtrackerurl=Страница баг-трекера -flatpak.info.currentreleasedate=Дата релиза -flatpak.info.currentreleasedescription=Описание версии -flatpak.info.currentreleaseversion=Версия -flatpak.info.donationurl=Страница пожертвований -flatpak.info.downloadflatpakrefurl=Ссылка -flatpak.info.helpurl=Страница поддержки -flatpak.info.homepageurl=Домашяя страница -flatpak.info.instoresincedate=Добавлено в -flatpak.info.projectlicense=Лицензия -flatpak.info.translateurl=Страница переводов -flatpak.info.developername=Разработчик -flatpak.info.arch=Архитектура -flatpak.info.branch=Ветка -flatpak.info.collection=Коллекция -flatpak.info.commit=Коммит -flatpak.info.date=Дата -flatpak.info.description=Описание -flatpak.info.id=Идентификатор -flatpak.info.installation=Тип установки -flatpak.info.installation.user=Пользователь -flatpak.info.installation.system=Система -flatpak.info.installed=Размер установки -flatpak.info.license=Лицензия -flatpak.info.name=Имя -flatpak.info.origin=Происхождение -flatpak.info.parent=Разработчик -flatpak.info.ref=Ссылка -flatpak.info.runtime=Среда выполнения -flatpak.info.sdk=Среда разработки -flatpak.info.subject=Субъект -flatpak.info.type=Тип -flatpak.info.version=Версия -flatpak.history.date=Дата -flatpak.history.commit=Коммит -flatpak.install.install_level.title=Тип установки -flatpak.install.install_level.body=Должен ли {} быть установлен для всех пользователей устройства (системы)? -flatpak.install.bad_install_level.body=Недопустимое значение для {field} в файле конфигурации {file} -flatpak.remotes.system_flathub.error=Не удалось добавить Flathub в качестве системного репозитория (удаленного) +flatpak.config.install_level=Уровень +flatpak.config.install_level.ask.tip={app} запросит уровень, который должен быть применен во время установки приложения flatpak.config.install_level.system=Система flatpak.config.install_level.system.tip=Приложения будут установлены для всех пользователей устройства flatpak.config.install_level.user=Пользователь flatpak.config.install_level.user.tip=Приложение будет установлено только для текущего пользователя -flatpak.config.install_level.ask=Спросить -flatpak.config.install_level.ask.tip={app} запросит уровень, который должен быть применен во время установки приложения -flatpak.config.install_level=Уровень \ No newline at end of file +flatpak.downgrade.commits=Чтение коммитов пакета +flatpak.downgrade.impossible.body=Невозможно понизить версию: приложение находится в первой версии +flatpak.downgrade.impossible.title=Ошибка +flatpak.downgrade.reverting=Возврат версии +flatpak.history.commit=Коммит +flatpak.history.date=Дата +flatpak.history.subject=subject +flatpak.info.arch=Архитектура +flatpak.info.branch=Ветка +flatpak.info.bugtrackerurl=Страница баг-трекера +flatpak.info.collection=Коллекция +flatpak.info.commit=Коммит +flatpak.info.currentreleasedate=Дата релиза +flatpak.info.currentreleasedescription=Описание версии +flatpak.info.currentreleaseversion=Версия +flatpak.info.date=Дата +flatpak.info.description=Описание +flatpak.info.developername=Разработчик +flatpak.info.donationurl=Страница пожертвований +flatpak.info.downloadflatpakrefurl=Ссылка +flatpak.info.flatpakappid=Идентификатор +flatpak.info.helpurl=Страница поддержки +flatpak.info.homepageurl=Домашяя страница +flatpak.info.id=Идентификатор +flatpak.info.installation=Тип установки +flatpak.info.installation.system=Система +flatpak.info.installation.user=Пользователь +flatpak.info.installed=Размер установки +flatpak.info.instoresincedate=Добавлено в +flatpak.info.license=Лицензия +flatpak.info.name=Имя +flatpak.info.origin=Происхождение +flatpak.info.parent=Разработчик +flatpak.info.projectlicense=Лицензия +flatpak.info.ref=Ссылка +flatpak.info.runtime=Среда выполнения +flatpak.info.sdk=Среда разработки +flatpak.info.subject=Субъект +flatpak.info.translateurl=Страница переводов +flatpak.info.type=Тип +flatpak.info.version=Версия +flatpak.install.bad_install_level.body=Недопустимое значение для {field} в файле конфигурации {file} +flatpak.install.install_level.body=Должен ли {} быть установлен для всех пользователей устройства (системы)? +flatpak.install.install_level.title=Тип установки +flatpak.notification.disable=Если вы не хотите использовать приложения Flatpak, снимите флажок {} в {} +flatpak.notification.no_remotes=Поддержка Flatpak не установлена. Поиск приложений Flatpak будет невозможен. +flatpak.remotes.system_flathub.error=Не удалось добавить Flathub в качестве системного репозитория (удаленного) +gem.flatpak.info=Приложения, доступные на Flathub и других репозиториях, настроенных в вашей системе \ No newline at end of file diff --git a/bauh/gems/snap/__init__.py b/bauh/gems/snap/__init__.py index c084fb7d..bfd47893 100644 --- a/bauh/gems/snap/__init__.py +++ b/bauh/gems/snap/__init__.py @@ -1,9 +1,14 @@ import os from bauh.api.constants import CACHE_PATH +from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) SNAP_CACHE_PATH = CACHE_PATH + '/snap' CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt' URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt' SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/suggestions.txt' + + +def get_icon_path() -> str: + return resource.get_path('img/snap.svg', ROOT_DIR) diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 0f0972b1..1c42f898 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -4,16 +4,17 @@ from datetime import datetime from threading import Thread from typing import List, Set, Type -from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext +from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess -from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE +from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE, \ + get_icon_path from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.model import SnapApplication from bauh.gems.snap.worker import SnapAsyncDataLoader @@ -33,8 +34,6 @@ class SnapManager(SoftwareManager): self.logger = context.logger self.ubuntu_distro = context.distro == 'ubuntu' self.categories = {} - self.categories_downloader = CategoriesDownloader('snap', self.http_client, self.logger, self, context.disk_cache, - URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH) self.suggestions_cache = context.cache_factory.new() self.info_path = None @@ -122,7 +121,6 @@ class SnapManager(SoftwareManager): info_path = self.get_info_path() if snap.is_snapd_running() and info_path: - self.categories_downloader.join() installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader, internet=internet_available) for app_json in snap.read_installed(info_path)] return SearchResult(installed, None, len(installed)) else: @@ -131,7 +129,7 @@ class SnapManager(SoftwareManager): def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None)) - def update(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> SystemProcess: + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> SystemProcess: raise Exception("'update' is not supported by {}".format(pkg.__class__.__name__)) def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: @@ -216,13 +214,25 @@ class SnapManager(SoftwareManager): return snap.is_installed() def requires_root(self, action: str, pkg: SnapApplication): - return action != 'search' + return action not in ('search', 'prepare') def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password))) - def prepare(self): - self.categories_downloader.start() + def _start_category_task(self, task_man: TaskManager): + task_man.register_task('snap_cats', self.i18n['task.download_categories'].format('Snap'), get_icon_path()) + task_man.update_progress('snap_cats', 50, None) + + def _finish_category_task(self, task_man: TaskManager): + task_man.update_progress('snap_cats', 100, None) + task_man.finish_task('snap_cats') + + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): + CategoriesDownloader(id_='snap', manager=self, http_client=self.http_client, logger=self.logger, + url_categories_file=URL_CATEGORIES_FILE, disk_cache_dir=SNAP_CACHE_PATH, + categories_path=CATEGORIES_FILE_PATH, + before=lambda: self._start_category_task(task_manager), + after=lambda: self._finish_category_task(task_manager)).start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: pass @@ -268,7 +278,6 @@ class SnapManager(SoftwareManager): return res else: self.logger.info('Mapping suggestions') - self.categories_downloader.join() suggestions, threads = [], [] installed = {i.name.lower() for i in self.read_installed(disk_loader=None).installed} if filter_installed else None diff --git a/bauh/gems/snap/model.py b/bauh/gems/snap/model.py index eefce57e..723211ee 100644 --- a/bauh/gems/snap/model.py +++ b/bauh/gems/snap/model.py @@ -1,15 +1,15 @@ from typing import List -from bauh.api.abstract.model import SoftwarePackage, PackageAction +from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction from bauh.commons import resource from bauh.gems.snap import ROOT_DIR EXTRA_INSTALLED_ACTIONS = [ - PackageAction(i18n_status_key='snap.action.refresh.status', - i18_label_key='snap.action.refresh.label', - icon_path=resource.get_path('img/refresh.svg', ROOT_DIR), - manager_method='refresh', - requires_root=True) + CustomSoftwareAction(i18n_status_key='snap.action.refresh.status', + i18_label_key='snap.action.refresh.label', + icon_path=resource.get_path('img/refresh.svg', ROOT_DIR), + manager_method='refresh', + requires_root=True) ] KNOWN_RUNTIME_NAMES = {'snapd', 'snapcraft', 'multipass'} @@ -79,6 +79,9 @@ class SnapApplication(SoftwarePackage): def get_publisher(self): return self.publisher - def get_custom_supported_actions(self) -> List[PackageAction]: + def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: if self.installed: return EXTRA_INSTALLED_ACTIONS + + def supports_backup(self) -> bool: + return True diff --git a/bauh/gems/snap/resources/locale/ca b/bauh/gems/snap/resources/locale/ca index 68bc77d6..d5c4966c 100644 --- a/bauh/gems/snap/resources/locale/ca +++ b/bauh/gems/snap/resources/locale/ca @@ -1,19 +1,19 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store +snap.action.refresh.label=Actualitza +snap.action.refresh.status=S’està actualitzant snap.info.commands=ordres snap.info.contact=contacte snap.info.description=descripció -snap.info.license=llicència -snap.info.revision=revisió -snap.info.tracking=seguiment snap.info.installed=instal·lada +snap.info.license=llicència snap.info.publisher=publicador -snap.info.version=versió +snap.info.revision=revisió snap.info.size=mida -snap.notification.snapd_unavailable=Sembla que no s’ha iniciat o activat {}. Els paquets {} no estaran disponibles. -snap.notification.snap.disable=Si no voleu utilitzar aplicacions snap, desmarqueu {} a {} -snap.notifications.api.unavailable=Sembla que l’API de {} no està disponible en aquests moments. No podreu cercar aplicacions {} noves. -snap.action.refresh.status=S’està actualitzant -snap.action.refresh.label=Actualitza -snap.install.available_channels.title=Canals disponibles -snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall n’hi ha els següents +snap.info.tracking=seguiment +snap.info.version=versió snap.install.available_channels.help=Seleccioneu-ne un si voleu continuar +snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall n’hi ha els següents +snap.install.available_channels.title=Canals disponibles +snap.notification.snap.disable=Si no voleu utilitzar aplicacions snap, desmarqueu {} a {} +snap.notification.snapd_unavailable=Sembla que no s’ha iniciat o activat {}. Els paquets {} no estaran disponibles. +snap.notifications.api.unavailable=Sembla que l’API de {} no està disponible en aquests moments. No podreu cercar aplicacions {} noves. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/de b/bauh/gems/snap/resources/locale/de index 445b779e..5883a71a 100644 --- a/bauh/gems/snap/resources/locale/de +++ b/bauh/gems/snap/resources/locale/de @@ -1,19 +1,19 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store -snap.notification.snapd_unavailable={} wurde nicht gestartet oder ist deaktiviert. {} Pakete werden nicht verfügbar sein. -snap.notification.snap.disable=Um keine Snap Anwerndungen zu nutzen deaktivieren {} unter {} -snap.notifications.api.unavailable=Es scheint als ob die {} API aktuell nicht verfügbar ist. Die Suche nach neuen {} Anwendungen ist nicht möglich. -snap.action.refresh.status=Erneuern snap.action.refresh.label=Erneuern -snap.install.available_channels.title=Verfügbare Channels -snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende -snap.install.available_channels.help=Wähle einen aus um fortzufahren +snap.action.refresh.status=Erneuern snap.info.commands=Kommandos snap.info.contact=Kontakt snap.info.description=Beschreibung -snap.info.license=Lizenz -snap.info.revision=Revision -snap.info.tracking=Tracking snap.info.installed=installiert +snap.info.license=Lizenz snap.info.publisher=Herausgeber +snap.info.revision=Revision +snap.info.size=Größe +snap.info.tracking=Tracking snap.info.version=Version -snap.info.size=Größe \ No newline at end of file +snap.install.available_channels.help=Wähle einen aus um fortzufahren +snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende +snap.install.available_channels.title=Verfügbare Channels +snap.notification.snap.disable=Um keine Snap Anwerndungen zu nutzen deaktivieren {} unter {} +snap.notification.snapd_unavailable={} wurde nicht gestartet oder ist deaktiviert. {} Pakete werden nicht verfügbar sein. +snap.notifications.api.unavailable=Es scheint als ob die {} API aktuell nicht verfügbar ist. Die Suche nach neuen {} Anwendungen ist nicht möglich. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index a6cd99a6..33dcfcbc 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -1,19 +1,19 @@ gem.snap.info=Applications published at https://snapcraft.io/store -snap.notification.snapd_unavailable={} seems not to be started or enabled. {} packages will not be available. -snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {} -snap.notifications.api.unavailable=It seems the {} API is unavailable at the moment. It will not be possible to search for new {} applications. -snap.action.refresh.status=Refreshing snap.action.refresh.label=Refresh -snap.install.available_channels.title=Available channels -snap.install.available_channels.message=There is no {} channel available for {}. But there are these below -snap.install.available_channels.help=Select one if you want to continue +snap.action.refresh.status=Refreshing snap.info.commands=commands snap.info.contact=contact snap.info.description=description -snap.info.license=license -snap.info.revision=revision -snap.info.tracking=tracking snap.info.installed=installed +snap.info.license=license snap.info.publisher=publisher +snap.info.revision=revision +snap.info.size=size +snap.info.tracking=tracking snap.info.version=version -snap.info.size=size \ No newline at end of file +snap.install.available_channels.help=Select one if you want to continue +snap.install.available_channels.message=There is no {} channel available for {}. But there are these below +snap.install.available_channels.title=Available channels +snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {} +snap.notification.snapd_unavailable={} seems not to be started or enabled. {} packages will not be available. +snap.notifications.api.unavailable=It seems the {} API is unavailable at the moment. It will not be possible to search for new {} applications. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index 544e969b..15319d96 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -1,19 +1,19 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store +snap.action.refresh.label=Actualizar +snap.action.refresh.status=Actualizando snap.info.commands=comandos snap.info.contact=contacto snap.info.description=descripción -snap.info.license=licencia -snap.info.revision=revisión -snap.info.tracking=tracking snap.info.installed=instalado +snap.info.license=licencia snap.info.publisher=publicador -snap.info.version=versión +snap.info.revision=revisión snap.info.size=tamaño -snap.notification.snapd_unavailable={} parece no estar inicializado o habilitado. Los paquetes {} estarán indisponibles. -snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} -snap.notifications.api.unavailable=Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}. -snap.action.refresh.status=Actualizando -snap.action.refresh.label=Actualizar -snap.install.available_channels.title=Canales disponibles +snap.info.tracking=tracking +snap.info.version=versión +snap.install.available_channels.help=Seleccione uno si desea continuar snap.install.available_channels.message=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo -snap.install.available_channels.help=Seleccione uno si desea continuar \ No newline at end of file +snap.install.available_channels.title=Canales disponibles +snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} +snap.notification.snapd_unavailable={} parece no estar inicializado o habilitado. Los paquetes {} estarán indisponibles. +snap.notifications.api.unavailable=Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/it b/bauh/gems/snap/resources/locale/it index 86b2d0dd..ed6a1d96 100644 --- a/bauh/gems/snap/resources/locale/it +++ b/bauh/gems/snap/resources/locale/it @@ -1,9 +1,19 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store -snap.notification.snapd_unavailable={} sembra non essere avviato o abilitato. {} i pacchetti non saranno disponibili. -snap.notification.snap.disable=Se non si desidera utilizzare le applicazioni Snap, deselezionare {} in {} -snap.notifications.api.unavailable=Sembra che l'API {} non sia al momento disponibile. Non sarà possibile cercare nuove {} applicazioni. -snap.action.refresh.status=Ripristinare snap.action.refresh.label=Ripristina -snap.install.available_channels.title=Canali disponibili -snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto +snap.action.refresh.status=Ripristinare +snap.info.commands=commands +snap.info.contact=contact +snap.info.description=description +snap.info.installed=installed +snap.info.license=license +snap.info.publisher=publisher +snap.info.revision=revision +snap.info.size=size +snap.info.tracking=tracking +snap.info.version=version snap.install.available_channels.help=Selezionane uno se vuoi continuare +snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto +snap.install.available_channels.title=Canali disponibili +snap.notification.snap.disable=Se non si desidera utilizzare le applicazioni Snap, deselezionare {} in {} +snap.notification.snapd_unavailable={} sembra non essere avviato o abilitato. {} i pacchetti non saranno disponibili. +snap.notifications.api.unavailable=Sembra che l'API {} non sia al momento disponibile. Non sarà possibile cercare nuove {} applicazioni. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index bf2f66e2..0b3ad650 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -1,19 +1,19 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store +snap.action.refresh.label=Atualizar +snap.action.refresh.status=Atualizando snap.info.commands=comandos snap.info.contact=contato snap.info.description=descrição -snap.info.license=licença -snap.info.revision=revisão -snap.info.tracking=tracking snap.info.installed=instalado +snap.info.license=licença snap.info.publisher=publicador -snap.info.version=versão +snap.info.revision=revisão snap.info.size=tamanho -snap.notification.snapd_unavailable={} parece não estar inicializado ou habilitado. Os pacotes {} estarão indisponíveis. -snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {} -snap.notifications.api.unavailable=Parece que a API de {} está indisponível no momento. Não será possível buscar novos aplicativos {}. -snap.action.refresh.status=Atualizando -snap.action.refresh.label=Atualizar -snap.install.available_channels.title=Canais disponíveis +snap.info.tracking=tracking +snap.info.version=versão +snap.install.available_channels.help=Selecione algum se quiser continuar snap.install.available_channels.message=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo -snap.install.available_channels.help=Selecione algum se quiser continuar \ No newline at end of file +snap.install.available_channels.title=Canais disponíveis +snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {} +snap.notification.snapd_unavailable={} parece não estar inicializado ou habilitado. Os pacotes {} estarão indisponíveis. +snap.notifications.api.unavailable=Parece que a API de {} está indisponível no momento. Não será possível buscar novos aplicativos {}. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/ru b/bauh/gems/snap/resources/locale/ru index 9e2fd5ba..1b3a3564 100644 --- a/bauh/gems/snap/resources/locale/ru +++ b/bauh/gems/snap/resources/locale/ru @@ -1,19 +1,19 @@ gem.snap.info=Приложения, опубликованные на https://snapcraft.io/store -snap.notification.snapd_unavailable={} кажется, не запускается и не включается. {} пакеты будут недоступны. -snap.notification.snap.disable=Если вы не хотите использовать приложения Snap, снимите флажок {} в {} -snap.notifications.api.unavailable=Похоже, что API {} в данный момент недоступен. Невозможно будет искать новые приложения {}. -snap.action.refresh.status=Обновляется snap.action.refresh.label=Обновить -snap.install.available_channels.title=Доступные каналы -snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже -snap.install.available_channels.help=Выберите один, если вы хотите продолжить +snap.action.refresh.status=Обновляется snap.info.commands=Команды snap.info.contact=Контакт snap.info.description=Описание -snap.info.license=Лицензия -snap.info.revision=Ревизия -snap.info.tracking=Отслеживание snap.info.installed=Размер установки +snap.info.license=Лицензия snap.info.publisher=Издатель +snap.info.revision=Ревизия +snap.info.size=Размер +snap.info.tracking=Отслеживание snap.info.version=Версия -snap.info.size=Размер \ No newline at end of file +snap.install.available_channels.help=Выберите один, если вы хотите продолжить +snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже +snap.install.available_channels.title=Доступные каналы +snap.notification.snap.disable=Если вы не хотите использовать приложения Snap, снимите флажок {} в {} +snap.notification.snapd_unavailable={} кажется, не запускается и не включается. {} пакеты будут недоступны. +snap.notifications.api.unavailable=Похоже, что API {} в данный момент недоступен. Невозможно будет искать новые приложения {}. \ No newline at end of file diff --git a/bauh/gems/web/__init__.py b/bauh/gems/web/__init__.py index 5fdeca49..47760f44 100644 --- a/bauh/gems/web/__init__.py +++ b/bauh/gems/web/__init__.py @@ -2,7 +2,7 @@ import os from pathlib import Path from bauh.api.constants import DESKTOP_ENTRIES_DIR, CONFIG_PATH, TEMP_DIR -from bauh.commons import user +from bauh.commons import user, resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) WEB_PATH = '{}/.local/share/bauh/web'.format(Path.home()) @@ -15,7 +15,7 @@ NODE_BIN_PATH = '{}/bin/node'.format(NODE_DIR_PATH) NPM_BIN_PATH = '{}/bin/npm'.format(NODE_DIR_PATH) NODE_MODULES_PATH = '{}/node_modules'.format(ENV_PATH) NATIVEFIER_BIN_PATH = '{}/.bin/nativefier'.format(NODE_MODULES_PATH) -ELECTRON_PATH = '{}/.cache/electron'.format(Path.home()) +ELECTRON_PATH = '{}/.cache/electron'.format(str(Path.home())) ELECTRON_DOWNLOAD_URL = 'https://github.com/electron/electron/releases/download/v{version}/electron-v{version}-linux-{arch}.zip' ELECTRON_SHA256_URL = 'https://github.com/electron/electron/releases/download/v{version}/SHASUMS256.txt' URL_ENVIRONMENT_SETTINGS = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/web/environment.yml' @@ -29,3 +29,6 @@ SUGGESTIONS_CACHE_FILE = '{}/suggestions.txt'.format(TEMP_PATH) CONFIG_FILE = '{}/web.yml'.format(CONFIG_PATH) URL_NATIVEFIER = 'https://github.com/jiahaog/nativefier/archive/v{version}.tar.gz' + +def get_icon_path() -> str: + return resource.get_path('img/web.svg', ROOT_DIR) diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index c57dec43..b4fd6088 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -17,10 +17,11 @@ from colorama import Fore from requests import exceptions, Response from bauh.api.abstract.context import ApplicationContext -from bauh.api.abstract.controller import SoftwareManager, SearchResult +from bauh.api.abstract.controller import SoftwareManager, SearchResult, UpgradeRequirements from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher -from bauh.api.abstract.model import SoftwarePackage, PackageAction, PackageSuggestion, PackageUpdate, PackageHistory, \ +from bauh.api.abstract.handler import ProcessWatcher, TaskManager +from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction, PackageSuggestion, PackageUpdate, \ + PackageHistory, \ SuggestionPriority, PackageStatus from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, SingleSelectComponent, \ SelectViewType, TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, PanelComponent @@ -28,9 +29,9 @@ from bauh.api.constants import DESKTOP_ENTRIES_DIR from bauh.commons import resource, user from bauh.commons.config import save_config from bauh.commons.html import bold -from bauh.commons.system import ProcessHandler, get_dir_size, get_human_size_str +from bauh.commons.system import ProcessHandler, get_dir_size, get_human_size_str, SimpleProcess from bauh.gems.web import INSTALLED_PATH, nativefier, DESKTOP_ENTRY_PATH_PATTERN, URL_FIX_PATTERN, ENV_PATH, UA_CHROME, \ - SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR, CONFIG_FILE, TEMP_PATH, FIXES_PATH + SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, ROOT_DIR, CONFIG_FILE, TEMP_PATH, FIXES_PATH, ELECTRON_PATH from bauh.gems.web.config import read_config from bauh.gems.web.environment import EnvironmentUpdater, EnvironmentComponent from bauh.gems.web.model import WebApplication @@ -68,7 +69,12 @@ class WebApplicationManager(SoftwareManager): self.env_thread = None self.suggestions_downloader = suggestions_downloader self.suggestions = {} - + self.custom_actions = [CustomSoftwareAction(i18_label_key='web.custom_action.clean_env', + i18n_status_key='web.custom_action.clean_env.status', + manager=self, + manager_method='clean_environment', + icon_path=resource.get_path('img/web.svg', ROOT_DIR), + requires_root=False)] def _get_lang_header(self) -> str: try: system_locale = locale.getdefaultlocale() @@ -76,6 +82,34 @@ class WebApplicationManager(SoftwareManager): except: return 'en_US' + def clean_environment(self, root_password: str, watcher: ProcessWatcher) -> bool: + handler = ProcessHandler(watcher) + + success = True + for path in (ENV_PATH, ELECTRON_PATH): + self.logger.info("Checking path '{}'".format(path)) + if os.path.exists(path): + try: + self.logger.info('Removing path {}'.format(path)) + res, output = handler.handle_simple(SimpleProcess(['rm', '-rf', path])) + + if not res: + success = False + except: + watcher.print(traceback.format_exc()) + success = False + + if success: + watcher.show_message(title=self.i18n['success'].capitalize(), + body=self.i18n['web.custom_action.clean_env.success'], + type_=MessageType.INFO) + else: + watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['web.custom_action.clean_env.failed'], + type_=MessageType.ERROR) + + return success + def _get_app_name(self, url_no_protocol: str, soup: "BeautifulSoup") -> str: name_tag = soup.head.find('meta', attrs={'name': 'application-name'}) name = name_tag.get('content') if name_tag else None @@ -304,7 +338,7 @@ class WebApplicationManager(SoftwareManager): def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: pass - def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: pass def uninstall(self, pkg: WebApplication, root_password: str, watcher: ProcessWatcher) -> bool: @@ -408,7 +442,7 @@ class WebApplicationManager(SoftwareManager): inp_desc = TextInputComponent(label=self.i18n['description'], value=app.description) cat_ops = [InputOption(label=self.i18n['web.install.option.category.none'].capitalize(), value=0)] - cat_ops.extend([InputOption(label=self.i18n[c.lower()].capitalize(), value=c) for c in self.context.default_categories]) + cat_ops.extend([InputOption(label=self.i18n.get('category.{}'.format(c.lower()), c).capitalize(), value=c) for c in self.context.default_categories]) def_cat = cat_ops[0] @@ -760,23 +794,24 @@ class WebApplicationManager(SoftwareManager): def requires_root(self, action: str, pkg: SoftwarePackage): return False - def _update_env_settings(self): - self.env_settings = self.env_updater.read_settings() + def _update_env_settings(self, task_manager: TaskManager = None): + self.env_settings = self.env_updater.read_settings(task_manager) - def _download_suggestions(self): - downloader = SuggestionsDownloader(logger=self.logger, http_client=self.http_client) + def _download_suggestions(self, taskman: TaskManager = None): + downloader = SuggestionsDownloader(logger=self.logger, http_client=self.http_client, i18n=self.i18n, taskman=taskman) self.suggestions = downloader.download() if self.suggestions: index_gen = SearchIndexGenerator(logger=self.logger) Thread(target=index_gen.generate_index, args=(self.suggestions,), daemon=True).start() - def prepare(self): - self.env_thread = Thread(target=self._update_env_settings, daemon=True) - self.env_thread.start() + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): + if internet_available: + self.env_thread = Thread(target=self._update_env_settings, args=(task_manager,), daemon=True) + self.env_thread.start() - self.suggestions_downloader = Thread(target=self._download_suggestions, daemon=True) - self.suggestions_downloader.start() + self.suggestions_downloader = Thread(target=self._download_suggestions, args=(task_manager,), daemon=True) + self.suggestions_downloader.start() def list_updates(self, internet_available: bool) -> List[PackageUpdate]: pass @@ -850,7 +885,7 @@ class WebApplicationManager(SoftwareManager): self.suggestions_downloader.join(5) suggestions = self.suggestions else: - suggestions = SuggestionsDownloader(logger=self.logger, http_client=self.http_client).download() + suggestions = SuggestionsDownloader(logger=self.logger, http_client=self.http_client, i18n=self.i18n).download() # cleaning memory self.suggestions_downloader = None @@ -897,7 +932,7 @@ class WebApplicationManager(SoftwareManager): return res - def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: + def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: pass def is_default_enabled(self) -> bool: @@ -970,3 +1005,6 @@ class WebApplicationManager(SoftwareManager): return True, None except: return False, [traceback.format_exc()] + + def get_custom_actions(self) -> List[CustomSoftwareAction]: + return self.custom_actions diff --git a/bauh/gems/web/environment.py b/bauh/gems/web/environment.py index 2457fbbc..7ef964ab 100644 --- a/bauh/gems/web/environment.py +++ b/bauh/gems/web/environment.py @@ -11,7 +11,7 @@ import requests import yaml from bauh.api.abstract.download import FileDownloader -from bauh.api.abstract.handler import ProcessWatcher +from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.view import MessageType from bauh.api.http import HttpClient from bauh.commons import system @@ -19,7 +19,7 @@ from bauh.commons.html import bold from bauh.commons.system import SimpleProcess, ProcessHandler from bauh.gems.web import ENV_PATH, NODE_DIR_PATH, NODE_BIN_PATH, NODE_MODULES_PATH, NATIVEFIER_BIN_PATH, \ ELECTRON_PATH, ELECTRON_DOWNLOAD_URL, ELECTRON_SHA256_URL, URL_ENVIRONMENT_SETTINGS, NPM_BIN_PATH, NODE_PATHS, \ - nativefier, URL_NATIVEFIER + nativefier, URL_NATIVEFIER, get_icon_path from bauh.gems.web.model import WebApplication from bauh.view.util.translation import I18n @@ -243,20 +243,33 @@ class EnvironmentUpdater: return res - def read_settings(self) -> dict: + def _finish_task_download_settings(self, task_man: TaskManager): + if task_man: + task_man.update_progress('web_down_sets', 100, None) + task_man.finish_task('web_down_sets') + + def read_settings(self, task_man: TaskManager = None) -> dict: try: + if task_man: + task_man.register_task('web_down_sets', self.i18n['web.task.download_settings'], get_icon_path()) + task_man.update_progress('web_down_sets', 10, None) + res = self.http_client.get(URL_ENVIRONMENT_SETTINGS) if not res: self.logger.warning('Could not retrieve the environments settings from the cloud') + self._finish_task_download_settings(task_man) return try: + self._finish_task_download_settings(task_man) return yaml.safe_load(res.content) except yaml.YAMLError: self.logger.error('Could not parse environment settings: {}'.format(res.text)) + self._finish_task_download_settings(task_man) return except requests.exceptions.ConnectionError: + self._finish_task_download_settings(task_man) return def _check_and_fill_electron(self, pkg: WebApplication, env: dict, local_config: dict, x86_x64: bool, output: List[EnvironmentComponent]): diff --git a/bauh/gems/web/model.py b/bauh/gems/web/model.py index a2f27657..c0238600 100644 --- a/bauh/gems/web/model.py +++ b/bauh/gems/web/model.py @@ -145,3 +145,6 @@ class WebApplication(SoftwarePackage): if config_dirs: return config_dirs[0] + + def supports_backup(self) -> bool: + return False diff --git a/bauh/gems/web/resources/locale/ca b/bauh/gems/web/resources/locale/ca new file mode 100644 index 00000000..1d5f5f05 --- /dev/null +++ b/bauh/gems/web/resources/locale/ca @@ -0,0 +1,75 @@ +gem.web.info=It allows to install Web applications on the system through their addresses ( URLs ) +gem.web.install.warning=This app it is not officially distributed by its domain owner +web.custom_action.clean_env.failed=An error occurred during the installation environment cleaning +web.custom_action.clean_env.status=Cleaning the installation environment +web.custom_action.clean_env.success=Installation environment cleaned +web.custom_action.clean_env=Clean installation environment +web.env.checking=Checking the Web installation environment +web.env.error=It seems there are issues with the Web installation environment. It wil not be possible to install {}. +web.environment.install=Installing {} +web.info.01_url=URL +web.info.02_description=description +web.info.03_version=version +web.info.04_categories=categories +web.info.05_installation_dir=installation dir +web.info.06_desktop_entry=shortcut +web.info.07_exec_file=executable +web.info.08_icon_path=icon +web.info.09_size=size +web.info.10_config_dir=configuration dir +web.install.env_update.body=The following files must be downloaded and installed +web.install.env_update.title=Environment update +web.install.error=An error has happened during the {} installation +web.install.global_nativefier.unavailable={n} seems not to be installed on your system. It will not be possible to install {app} +web.install.nativefier.error.inner_dir=The app installation directory was not found in {} +web.install.nativefier.error.unknown=Have a look at the {} to identify the reason +web.install.option.allow_urls.label=Allow internal URLs +web.install.option.allow_urls.tip=It allows the application to internally open addresses / URLs outside its domain. e.g: outlook.live.com would be able to open account.microsoft.com +web.install.option.category.none=none +web.install.option.fullscreen.label=Open in fullscreen +web.install.option.fullscreen.tip=If the app should always be opened in fullscreen ( the Alt key opens the upper menu ) +web.install.option.icon.label=Custom icon +web.install.option.ignore_certificate.label=Ignore certificate errors +web.install.option.ignore_certificate.tip=Certificate related errors will be ignored by the app +web.install.option.insecure.label=Allow insecure content +web.install.option.insecure.tip=It allows the execution of insecure content within the application +web.install.option.max.label=Open maximized +web.install.option.max.tip=If the app should always be opened maximized +web.install.option.nocache.label=No cache +web.install.option.nocache.tip=Session data will not be stored after the app is closed +web.install.option.noframe.label=No frame +web.install.option.noframe.tip=If the app should not have a frame +web.install.option.single.label=Single run +web.install.option.single.tip=It will not allow the app to be opened again if it is already opened +web.install.option.tray.default.label=Open and attach +web.install.option.tray.default.tip=The app icon will be attached to the system tray after being opened +web.install.option.tray.label=Tray mode +web.install.option.tray.min.label=Start minimized +web.install.option.tray.min.tip=The app will be started minimized as an icon in the system tray +web.install.option.tray.off.label=Off +web.install.option.tray.off.tip=Off +web.install.option.wicon.deducted.label=Deducted +web.install.option.wicon.deducted.tip=The icon will be deducted by {} during the installation +web.install.option.wicon.displayed.label=Displayed +web.install.option.wicon.displayed.tip=The icon displayed on the table will be used for the installation +web.install.option.wicon.label=Icon +web.install.options.advanced=advanced +web.install.options.basic=basic +web.install.options_dialog.title=Installation options +web.install.substatus.call_nativefier=Running {} +web.install.substatus.checking_fixes=Checking if there are published fixes +web.install.substatus.options=Waiting for the installation options +web.install.substatus.shortcut=Generating a menu shortcut +web.settings.electron.version.label=Electron version +web.settings.electron.version.tooltip=Defines an alternative Electron version to render the new installed apps +web.settings.env.nativefier.system.not_installed={} seems not to be installed on your system +web.settings.nativefier.env.tooltip=The Nativefier version installed on the isolated {app} environment will be used to install applications +web.settings.nativefier.env=environment +web.settings.nativefier.system.tooltip=The Nativefier version installed on your system will be used to install applications +web.settings.nativefier.system=system +web.settings.nativefier.tip=Defines which Nativefier version should be used to generate the Web applications +web.task.download_settings=Updating environment settings +web.task.suggestions=Downloading applications suggestions list +web.uninstall.error.install_dir.not_found=The installation directory {} was not found +web.uninstall.error.remove_dir=It was not possible to remove {} +web.waiting.env_updater=Updating environment \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en index 136433da..1d5f5f05 100644 --- a/bauh/gems/web/resources/locale/en +++ b/bauh/gems/web/resources/locale/en @@ -1,54 +1,12 @@ -gem.web.info=It allows to install Web applications on the system through they addresses ( URLs ) +gem.web.info=It allows to install Web applications on the system through their addresses ( URLs ) gem.web.install.warning=This app it is not officially distributed by its domain owner -web.environment.install=Installing {} -web.waiting.env_updater=Updating environment +web.custom_action.clean_env.failed=An error occurred during the installation environment cleaning +web.custom_action.clean_env.status=Cleaning the installation environment +web.custom_action.clean_env.success=Installation environment cleaned +web.custom_action.clean_env=Clean installation environment web.env.checking=Checking the Web installation environment web.env.error=It seems there are issues with the Web installation environment. It wil not be possible to install {}. -web.install.env_update.title=Environment update -web.install.env_update.body=The following files must be downloaded and installed -web.install.options.basic=basic -web.install.options.advanced=advanced -web.install.options_dialog.title=Installation options -web.install.error=An error has happened during the {} installation -web.install.nativefier.error.unknown=Have a look at the {} to identify the reason -web.install.nativefier.error.inner_dir=The app installation directory was not found in {} -web.install.substatus.call_nativefier=Running {} -web.install.substatus.shortcut=Generating a menu shortcut -web.install.substatus.options=Waiting for the installation options -web.install.substatus.checking_fixes=Checking if there are published fixes -web.install.option.single.label=Single run -web.install.option.single.tip=It will not allow the app to be opened again if it is already opened -web.install.option.max.label=Open maximized -web.install.option.max.tip=If the app should always be opened maximized -web.install.option.fullscreen.label=Open in fullscreen -web.install.option.fullscreen.tip=If the app should always be opened in fullscreen ( the Alt key opens the upper menu ) -web.install.option.noframe.label=No frame -web.install.option.noframe.tip=If the app should not have a frame -web.install.option.nocache.label=No cache -web.install.option.nocache.tip=Session data will not be stored after the app is closed -web.install.option.insecure.label=Allow insecure content -web.install.option.insecure.tip=It allows the execution of insecure content within the application -web.install.option.ignore_certificate.label=Ignore certificate errors -web.install.option.ignore_certificate.tip=Certificate related errors will be ignored by the app -web.install.option.allow_urls.label=Allow internal URLs -web.install.option.allow_urls.tip=It allows the application to internally open addresses / URLs outside its domain. e.g: outlook.live.com would be able to open account.microsoft.com -web.install.option.tray.label=Tray mode -web.install.option.tray.off.label=Off -web.install.option.tray.off.tip=Off -web.install.option.tray.default.label=Open and attach -web.install.option.tray.default.tip=The app icon will be attached to the system tray after being opened -web.install.option.tray.min.label=Start minimized -web.install.option.tray.min.tip=The app will be started minimized as an icon in the system tray -web.install.option.category.none=none -web.install.option.icon.label=Custom icon -web.install.option.wicon.label=Icon -web.install.option.wicon.deducted.label=Deducted -web.install.option.wicon.deducted.tip=The icon will be deducted by {} during the installation -web.install.option.wicon.displayed.label=Displayed -web.install.option.wicon.displayed.tip=The icon displayed on the table will be used for the installation -web.install.global_nativefier.unavailable={n} seems not to be installed on your system. It will not be possible to install {app} -web.uninstall.error.install_dir.not_found=The installation directory {} was not found -web.uninstall.error.remove_dir=It was not possible to remove {} +web.environment.install=Installing {} web.info.01_url=URL web.info.02_description=description web.info.03_version=version @@ -59,11 +17,59 @@ web.info.07_exec_file=executable web.info.08_icon_path=icon web.info.09_size=size web.info.10_config_dir=configuration dir +web.install.env_update.body=The following files must be downloaded and installed +web.install.env_update.title=Environment update +web.install.error=An error has happened during the {} installation +web.install.global_nativefier.unavailable={n} seems not to be installed on your system. It will not be possible to install {app} +web.install.nativefier.error.inner_dir=The app installation directory was not found in {} +web.install.nativefier.error.unknown=Have a look at the {} to identify the reason +web.install.option.allow_urls.label=Allow internal URLs +web.install.option.allow_urls.tip=It allows the application to internally open addresses / URLs outside its domain. e.g: outlook.live.com would be able to open account.microsoft.com +web.install.option.category.none=none +web.install.option.fullscreen.label=Open in fullscreen +web.install.option.fullscreen.tip=If the app should always be opened in fullscreen ( the Alt key opens the upper menu ) +web.install.option.icon.label=Custom icon +web.install.option.ignore_certificate.label=Ignore certificate errors +web.install.option.ignore_certificate.tip=Certificate related errors will be ignored by the app +web.install.option.insecure.label=Allow insecure content +web.install.option.insecure.tip=It allows the execution of insecure content within the application +web.install.option.max.label=Open maximized +web.install.option.max.tip=If the app should always be opened maximized +web.install.option.nocache.label=No cache +web.install.option.nocache.tip=Session data will not be stored after the app is closed +web.install.option.noframe.label=No frame +web.install.option.noframe.tip=If the app should not have a frame +web.install.option.single.label=Single run +web.install.option.single.tip=It will not allow the app to be opened again if it is already opened +web.install.option.tray.default.label=Open and attach +web.install.option.tray.default.tip=The app icon will be attached to the system tray after being opened +web.install.option.tray.label=Tray mode +web.install.option.tray.min.label=Start minimized +web.install.option.tray.min.tip=The app will be started minimized as an icon in the system tray +web.install.option.tray.off.label=Off +web.install.option.tray.off.tip=Off +web.install.option.wicon.deducted.label=Deducted +web.install.option.wicon.deducted.tip=The icon will be deducted by {} during the installation +web.install.option.wicon.displayed.label=Displayed +web.install.option.wicon.displayed.tip=The icon displayed on the table will be used for the installation +web.install.option.wicon.label=Icon +web.install.options.advanced=advanced +web.install.options.basic=basic +web.install.options_dialog.title=Installation options +web.install.substatus.call_nativefier=Running {} +web.install.substatus.checking_fixes=Checking if there are published fixes +web.install.substatus.options=Waiting for the installation options +web.install.substatus.shortcut=Generating a menu shortcut web.settings.electron.version.label=Electron version web.settings.electron.version.tooltip=Defines an alternative Electron version to render the new installed apps -web.settings.nativefier.env=environment -web.settings.nativefier.system=system +web.settings.env.nativefier.system.not_installed={} seems not to be installed on your system web.settings.nativefier.env.tooltip=The Nativefier version installed on the isolated {app} environment will be used to install applications +web.settings.nativefier.env=environment web.settings.nativefier.system.tooltip=The Nativefier version installed on your system will be used to install applications +web.settings.nativefier.system=system web.settings.nativefier.tip=Defines which Nativefier version should be used to generate the Web applications -web.settings.env.nativefier.system.not_installed={} seems not to be installed on your system \ No newline at end of file +web.task.download_settings=Updating environment settings +web.task.suggestions=Downloading applications suggestions list +web.uninstall.error.install_dir.not_found=The installation directory {} was not found +web.uninstall.error.remove_dir=It was not possible to remove {} +web.waiting.env_updater=Updating environment \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/es b/bauh/gems/web/resources/locale/es index e38065ea..04253f2d 100644 --- a/bauh/gems/web/resources/locale/es +++ b/bauh/gems/web/resources/locale/es @@ -1,54 +1,12 @@ gem.web.info=Le permite instalar aplicaciones web a través de sus direcciones (URLs) gem.web.install.warning=Esta aplicación no es distribuida oficialmente por el propietario del dominio -web.environment.install=Instalando {} -web.waiting.env_updater=Actualizando el ambiente +web.custom_action.clean_env.failed=Se produjo un error durante la limpieza del ambiente de instalación +web.custom_action.clean_env.status=Limpiando el ambiente de instalación +web.custom_action.clean_env.success=Ambiente de instalación limpio +web.custom_action.clean_env=Limpiar ambiente de instalación web.env.checking=Verificando el ambiente de instalación web web.env.error=Parece que hay problemas con el ambiente de instalación web. No será posible instalar {}. -web.install.env_update.title=Actualización del ambiente -web.install.env_update.body=Los siguientes archivos deben descargarse e instalarse -web.install.options.basic=básicas -web.install.options.advanced=avanzadas -web.install.options_dialog.title=Opciones de instalación -web.install.error=Ha ocurrido un error durante la instalación de {} -web.install.nativefier.error.unknown=Mire el {} para identificar la razón -web.install.nativefier.error.inner_dir=El directorio de instalación de la aplicación no se encontró en {} -web.install.substatus.call_nativefier=Ejecutando {} -web.install.substatus.shortcut=Generando un atajo de menú -web.install.substatus.options=Esperando las opciones de instalación -web.install.substatus.checking_fixes=Verificando si hay correcciones publicadas -web.install.option.single.label=Ejecución única -web.install.option.single.tip=No permitirá que la aplicación se abra nuevamente si ya está abierta -web.install.option.max.label=Abrir maximizado -web.install.option.max.tip=Si la aplicación siempre debe abrirse maximizada -web.install.option.fullscreen.label=Abrir en pantalla completa -web.install.option.fullscreen.tip=Si la aplicación siempre debe abrirse en pantalla completa ( la tecla Alt abre el menú superior ) -web.install.option.noframe.label=Sin marco -web.install.option.noframe.tip=Si la aplicación no debe tener un marco -web.install.option.nocache.label=Sin cache -web.install.option.nocache.tip=Los datos de la sesión no se almacenarán después de cerrar la aplicación -web.install.option.insecure.label=Permitir contenido inseguro -web.install.option.insecure.tip=Permite la ejecución de contenido inseguro dentro de la aplicación -web.install.option.ignore_certificate.label=Ignorar errores de certificado -web.install.option.ignore_certificate.tip=Los errores relacionados con certificados serán ignorados por la aplicación -web.install.option.allow_urls.label=Permitir URL internas -web.install.option.allow_urls.tip=Permite que la aplicación abra internamente direcciones / URLs fuera de su dominio. Por ejemplo: outlook.live.com podría abrir account.microsoft.com -web.install.option.tray.label=Modo bandeja -web.install.option.tray.off.label=Apagado -web.install.option.tray.off.tip=Apagado -web.install.option.tray.default.label=Abrir y adjuntar -web.install.option.tray.default.tip=El icono de la aplicación se adjuntará a la bandeja del sistema después de abrirlo -web.install.option.tray.min.label=Iniciar minimizado -web.install.option.tray.min.tip=La aplicación se iniciará minimizada como un ícono en la bandeja del sistema -web.install.option.category.none=ninguna -web.install.option.icon.label=Icono personalizado -web.install.option.wicon.label=Icono -web.install.option.wicon.deducted.label=Deducido -web.install.option.wicon.deducted.tip=El {} deducirá el icono durante la instalación -web.install.option.wicon.displayed.label=Mostrado -web.install.option.wicon.displayed.tip=El icono mostrado en la tabla será utilizado para la instalación -web.install.global_nativefier.unavailable={n} parece no estar instalado en su sistema. No será posible instalar {app} -web.uninstall.error.install_dir.not_found=No se encontró el directorio de instalación {} -web.uninstall.error.remove_dir=No fue posible eliminar {} +web.environment.install=Instalando {} web.info.01_url=URL web.info.02_description=descripción web.info.03_version=versión @@ -59,11 +17,59 @@ web.info.07_exec_file=ejecutable web.info.08_icon_path=icono web.info.09_size=tamaño web.info.10_config_dir=directorio de configuración +web.install.env_update.body=Los siguientes archivos deben descargarse e instalarse +web.install.env_update.title=Actualización del ambiente +web.install.error=Ha ocurrido un error durante la instalación de {} +web.install.global_nativefier.unavailable={n} parece no estar instalado en su sistema. No será posible instalar {app} +web.install.nativefier.error.inner_dir=El directorio de instalación de la aplicación no se encontró en {} +web.install.nativefier.error.unknown=Mire el {} para identificar la razón +web.install.option.allow_urls.label=Permitir URL internas +web.install.option.allow_urls.tip=Permite que la aplicación abra internamente direcciones / URLs fuera de su dominio. Por ejemplo: outlook.live.com podría abrir account.microsoft.com +web.install.option.category.none=ninguna +web.install.option.fullscreen.label=Abrir en pantalla completa +web.install.option.fullscreen.tip=Si la aplicación siempre debe abrirse en pantalla completa ( la tecla Alt abre el menú superior ) +web.install.option.icon.label=Icono personalizado +web.install.option.ignore_certificate.label=Ignorar errores de certificado +web.install.option.ignore_certificate.tip=Los errores relacionados con certificados serán ignorados por la aplicación +web.install.option.insecure.label=Permitir contenido inseguro +web.install.option.insecure.tip=Permite la ejecución de contenido inseguro dentro de la aplicación +web.install.option.max.label=Abrir maximizado +web.install.option.max.tip=Si la aplicación siempre debe abrirse maximizada +web.install.option.nocache.label=Sin cache +web.install.option.nocache.tip=Los datos de la sesión no se almacenarán después de cerrar la aplicación +web.install.option.noframe.label=Sin marco +web.install.option.noframe.tip=Si la aplicación no debe tener un marco +web.install.option.single.label=Ejecución única +web.install.option.single.tip=No permitirá que la aplicación se abra nuevamente si ya está abierta +web.install.option.tray.default.label=Abrir y adjuntar +web.install.option.tray.default.tip=El icono de la aplicación se adjuntará a la bandeja del sistema después de abrirlo +web.install.option.tray.label=Modo bandeja +web.install.option.tray.min.label=Iniciar minimizado +web.install.option.tray.min.tip=La aplicación se iniciará minimizada como un ícono en la bandeja del sistema +web.install.option.tray.off.label=Apagado +web.install.option.tray.off.tip=Apagado +web.install.option.wicon.deducted.label=Deducido +web.install.option.wicon.deducted.tip=El {} deducirá el icono durante la instalación +web.install.option.wicon.displayed.label=Mostrado +web.install.option.wicon.displayed.tip=El icono mostrado en la tabla será utilizado para la instalación +web.install.option.wicon.label=Icono +web.install.options.advanced=avanzadas +web.install.options.basic=básicas +web.install.options_dialog.title=Opciones de instalación +web.install.substatus.call_nativefier=Ejecutando {} +web.install.substatus.checking_fixes=Verificando si hay correcciones publicadas +web.install.substatus.options=Esperando las opciones de instalación +web.install.substatus.shortcut=Generando un atajo de menú web.settings.electron.version.label=Versión del Electron web.settings.electron.version.tooltip=Define una versión alternativa del Electron para renderizar las nuevas aplicaciones instaladas -web.settings.nativefier.env=ambiente -web.settings.nativefier.system=sistema +web.settings.env.nativefier.system.not_installed={} parece no estar instalado en su sistema web.settings.nativefier.env.tooltip=Se utilizará la versión de Nativefier instalada en el ambiente aislado de {app} para instalar aplicaciones +web.settings.nativefier.env=ambiente web.settings.nativefier.system.tooltip=Se utilizará la versión de Nativefier instalada en su sistema para instalar aplicaciones +web.settings.nativefier.system=sistema web.settings.nativefier.tip=Define qué versión de Nativefier debe usarse para generar las aplicaciones Web -web.settings.env.nativefier.system.not_installed={} parece no estar instalado en su sistema \ No newline at end of file +web.task.download_settings=Actualizando configuraciones de ambiente +web.task.suggestions=Descargando la lista de sugerencias de aplicaciones +web.uninstall.error.install_dir.not_found=No se encontró el directorio de instalación {} +web.uninstall.error.remove_dir=No fue posible eliminar {} +web.waiting.env_updater=Actualizando el ambiente \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/it b/bauh/gems/web/resources/locale/it new file mode 100644 index 00000000..1d5f5f05 --- /dev/null +++ b/bauh/gems/web/resources/locale/it @@ -0,0 +1,75 @@ +gem.web.info=It allows to install Web applications on the system through their addresses ( URLs ) +gem.web.install.warning=This app it is not officially distributed by its domain owner +web.custom_action.clean_env.failed=An error occurred during the installation environment cleaning +web.custom_action.clean_env.status=Cleaning the installation environment +web.custom_action.clean_env.success=Installation environment cleaned +web.custom_action.clean_env=Clean installation environment +web.env.checking=Checking the Web installation environment +web.env.error=It seems there are issues with the Web installation environment. It wil not be possible to install {}. +web.environment.install=Installing {} +web.info.01_url=URL +web.info.02_description=description +web.info.03_version=version +web.info.04_categories=categories +web.info.05_installation_dir=installation dir +web.info.06_desktop_entry=shortcut +web.info.07_exec_file=executable +web.info.08_icon_path=icon +web.info.09_size=size +web.info.10_config_dir=configuration dir +web.install.env_update.body=The following files must be downloaded and installed +web.install.env_update.title=Environment update +web.install.error=An error has happened during the {} installation +web.install.global_nativefier.unavailable={n} seems not to be installed on your system. It will not be possible to install {app} +web.install.nativefier.error.inner_dir=The app installation directory was not found in {} +web.install.nativefier.error.unknown=Have a look at the {} to identify the reason +web.install.option.allow_urls.label=Allow internal URLs +web.install.option.allow_urls.tip=It allows the application to internally open addresses / URLs outside its domain. e.g: outlook.live.com would be able to open account.microsoft.com +web.install.option.category.none=none +web.install.option.fullscreen.label=Open in fullscreen +web.install.option.fullscreen.tip=If the app should always be opened in fullscreen ( the Alt key opens the upper menu ) +web.install.option.icon.label=Custom icon +web.install.option.ignore_certificate.label=Ignore certificate errors +web.install.option.ignore_certificate.tip=Certificate related errors will be ignored by the app +web.install.option.insecure.label=Allow insecure content +web.install.option.insecure.tip=It allows the execution of insecure content within the application +web.install.option.max.label=Open maximized +web.install.option.max.tip=If the app should always be opened maximized +web.install.option.nocache.label=No cache +web.install.option.nocache.tip=Session data will not be stored after the app is closed +web.install.option.noframe.label=No frame +web.install.option.noframe.tip=If the app should not have a frame +web.install.option.single.label=Single run +web.install.option.single.tip=It will not allow the app to be opened again if it is already opened +web.install.option.tray.default.label=Open and attach +web.install.option.tray.default.tip=The app icon will be attached to the system tray after being opened +web.install.option.tray.label=Tray mode +web.install.option.tray.min.label=Start minimized +web.install.option.tray.min.tip=The app will be started minimized as an icon in the system tray +web.install.option.tray.off.label=Off +web.install.option.tray.off.tip=Off +web.install.option.wicon.deducted.label=Deducted +web.install.option.wicon.deducted.tip=The icon will be deducted by {} during the installation +web.install.option.wicon.displayed.label=Displayed +web.install.option.wicon.displayed.tip=The icon displayed on the table will be used for the installation +web.install.option.wicon.label=Icon +web.install.options.advanced=advanced +web.install.options.basic=basic +web.install.options_dialog.title=Installation options +web.install.substatus.call_nativefier=Running {} +web.install.substatus.checking_fixes=Checking if there are published fixes +web.install.substatus.options=Waiting for the installation options +web.install.substatus.shortcut=Generating a menu shortcut +web.settings.electron.version.label=Electron version +web.settings.electron.version.tooltip=Defines an alternative Electron version to render the new installed apps +web.settings.env.nativefier.system.not_installed={} seems not to be installed on your system +web.settings.nativefier.env.tooltip=The Nativefier version installed on the isolated {app} environment will be used to install applications +web.settings.nativefier.env=environment +web.settings.nativefier.system.tooltip=The Nativefier version installed on your system will be used to install applications +web.settings.nativefier.system=system +web.settings.nativefier.tip=Defines which Nativefier version should be used to generate the Web applications +web.task.download_settings=Updating environment settings +web.task.suggestions=Downloading applications suggestions list +web.uninstall.error.install_dir.not_found=The installation directory {} was not found +web.uninstall.error.remove_dir=It was not possible to remove {} +web.waiting.env_updater=Updating environment \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt index 8e5de011..2ada9b48 100644 --- a/bauh/gems/web/resources/locale/pt +++ b/bauh/gems/web/resources/locale/pt @@ -1,54 +1,12 @@ gem.web.info=Permite instalar aplicações Web através dos seus endereços ( URLs ) gem.web.install.warning=Esse aplicativo não é oficialmente distribuído pelo proprietário do domínio -web.environment.nativefier=Instalando {} -web.waiting.env_updater=Atualizando ambiente +web.custom_action.clean_env.failed=Ocorreu um problema durante a limpeza do ambiente de instalação +web.custom_action.clean_env.status=Limpando ambiente de instalação +web.custom_action.clean_env.success=Ambiente de instalação limpo +web.custom_action.clean_env=Limpar ambiente de instalação web.env.checking=Verificando o ambiente de instalação Web web.env.error=Parce que existem problemas com o ambiente de instalação Web. Não será possível instalar {}. -web.install.env_update.title=Atualização de ambiente -web.install.env_update.body=Os seguintes arquivo precisam ser baixados e instalados -web.install.options.basic=básicas -web.install.options.advanced=avançadas -web.install.options_dialog.title=Opções de instalação -wen.install.error=Ocorreu um erro durante a instalação de {} -web.install.nativefier.error.unknown=Dê uma olhada em {} para identificar o motivo -web.install.nativefier.error.inner_dir=O diretório de instalação do aplicativo não foi encontrado em {} -web.install.substatus.call_nativefier=Executando {} -web.install.substatus.shortcut=Criando um atalho no menu -web.install.substatus.options=Aguardando as opções de instalação -web.install.substatus.checking_fixes=Verificando se há correções publicadas -web.install.option.single.label=Execução única -web.install.option.single.tip=Não permitirá que o aplicativo seja aberto novamente caso o mesmo já esteja -web.install.option.max.label=Abrir maximizado -web.install.option.max.tip=Se o aplicativo deve sempre ser aberto maximizado -web.install.option.fullscreen.label=Abrir em tela cheia -web.install.option.fullscreen.tip=Se o aplicativo sempre deve ser aberto em tela cheia ( a tecla Alt abre o menu superior ) -web.install.option.noframe.label=Sem moldura -web.install.option.noframe.tip=Se o aplicativo não deve ter uma moldura -web.install.option.nocache.label=Sem cache -web.install.option.nocache.tip=Dados de sessão não serão armazenados após o aplicativo ser fechado -web.install.option.insecure.label=Permitir conteúdo não confiável -web.install.option.insecure.tip=Permite a execução de conteúdo não confiável dentro do aplicativo -web.install.option.ignore_certificate.label=Ignorar erros de certificado -web.install.option.ignore_certificate.tip=Erros associados a certificados serão ignorados pelo aplicativo -web.install.option.allow_urls.label=Permitir URLs internas -web.install.option.allow_urls.tip=Permite que o aplicativo abra internamente endereços / URLs fora do seu domínio. Exemplo: outlook.live.com conseguiria abrir account.microsoft.com -web.install.option.tray.label=Modo bandeja -web.install.option.tray.off.label=Desligado -web.install.option.tray.off.tip=Desligado -web.install.option.tray.default.label=Abrir e anexar -web.install.option.tray.default.tip=O ícone do aplicativo será anexado a bandeja do sistema após aberto -web.install.option.tray.min.label=Iniciar minimizado -web.install.option.tray.min.tip=O aplicativo será iniciado minimizado como um ícone da bandeja do sistema -web.install.option.category.none=nenhuma -web.install.option.icon.label=Ícone alternativo -web.install.option.wicon.label=Ícone -web.install.option.wicon.deducted.label=Deduzido -web.install.option.wicon.deducted.tip=O ícone será deduzido pelo {} durante a instalação -web.install.option.wicon.displayed.label=Exibido -web.install.option.wicon.displayed.tip=O ícone exibido na tabela será utilizado para a instalação -web.install.global_nativefier.unavailable={n} não parece estar instalado no seu sistema. Não será possível instalar {app} -web.uninstall.error.install_dir.not_found=O diretório de instalação {} não foi encontrado -web.uninstall.error.remove=Não foi possível remover {} +web.environment.nativefier=Instalando {} web.info.01_url=URL web.info.02_description=descrição web.info.03_version=versão @@ -59,11 +17,59 @@ web.info.07_exec_file=executável web.info.08_icon_path=ícone web.info.09_size=tamanho web.info.10_config_dir=diretório de configuração +web.install.env_update.body=Os seguintes arquivo precisam ser baixados e instalados +web.install.env_update.title=Atualização de ambiente +web.install.global_nativefier.unavailable={n} não parece estar instalado no seu sistema. Não será possível instalar {app} +web.install.nativefier.error.inner_dir=O diretório de instalação do aplicativo não foi encontrado em {} +web.install.nativefier.error.unknown=Dê uma olhada em {} para identificar o motivo +web.install.option.allow_urls.label=Permitir URLs internas +web.install.option.allow_urls.tip=Permite que o aplicativo abra internamente endereços / URLs fora do seu domínio. Exemplo: outlook.live.com conseguiria abrir account.microsoft.com +web.install.option.category.none=nenhuma +web.install.option.fullscreen.label=Abrir em tela cheia +web.install.option.fullscreen.tip=Se o aplicativo sempre deve ser aberto em tela cheia ( a tecla Alt abre o menu superior ) +web.install.option.icon.label=Ícone alternativo +web.install.option.ignore_certificate.label=Ignorar erros de certificado +web.install.option.ignore_certificate.tip=Erros associados a certificados serão ignorados pelo aplicativo +web.install.option.insecure.label=Permitir conteúdo não confiável +web.install.option.insecure.tip=Permite a execução de conteúdo não confiável dentro do aplicativo +web.install.option.max.label=Abrir maximizado +web.install.option.max.tip=Se o aplicativo deve sempre ser aberto maximizado +web.install.option.nocache.label=Sem cache +web.install.option.nocache.tip=Dados de sessão não serão armazenados após o aplicativo ser fechado +web.install.option.noframe.label=Sem moldura +web.install.option.noframe.tip=Se o aplicativo não deve ter uma moldura +web.install.option.single.label=Execução única +web.install.option.single.tip=Não permitirá que o aplicativo seja aberto novamente caso o mesmo já esteja +web.install.option.tray.default.label=Abrir e anexar +web.install.option.tray.default.tip=O ícone do aplicativo será anexado a bandeja do sistema após aberto +web.install.option.tray.label=Modo bandeja +web.install.option.tray.min.label=Iniciar minimizado +web.install.option.tray.min.tip=O aplicativo será iniciado minimizado como um ícone da bandeja do sistema +web.install.option.tray.off.label=Desligado +web.install.option.tray.off.tip=Desligado +web.install.option.wicon.deducted.label=Deduzido +web.install.option.wicon.deducted.tip=O ícone será deduzido pelo {} durante a instalação +web.install.option.wicon.displayed.label=Exibido +web.install.option.wicon.displayed.tip=O ícone exibido na tabela será utilizado para a instalação +web.install.option.wicon.label=Ícone +web.install.options.advanced=avançadas +web.install.options.basic=básicas +web.install.options_dialog.title=Opções de instalação +web.install.substatus.call_nativefier=Executando {} +web.install.substatus.checking_fixes=Verificando se há correções publicadas +web.install.substatus.options=Aguardando as opções de instalação +web.install.substatus.shortcut=Criando um atalho no menu web.settings.electron.version.label=Versão do Electron web.settings.electron.version.tooltip=Define uma versão alternativa do Electron para renderizar os novos aplicativos instalados -web.settings.nativefier.env=ambiente -web.settings.nativefier.system=sistema +web.settings.env.nativefier.system.not_installed={} não parece estar instalado no seu sistema web.settings.nativefier.env.tooltip=A versão do Nativefier instalada no ambiente isolado do {app} será utilizada para instalar aplicativos +web.settings.nativefier.env=ambiente web.settings.nativefier.system.tooltip=A versão do Nativefier instalada no seu sistema será utilizada para instalar aplicativos +web.settings.nativefier.system=sistema web.settings.nativefier.tip=Define qual versão do Nativefier será utilizada para gerar os aplicativos Web -web.settings.env.nativefier.system.not_installed={} não parece estar instalado no seu sistema \ No newline at end of file +web.task.download_settings=Atualizando configurações de ambiente +web.task.suggestions=Baixando lista de sugestões de aplicativos +web.uninstall.error.install_dir.not_found=O diretório de instalação {} não foi encontrado +web.uninstall.error.remove=Não foi possível remover {} +web.waiting.env_updater=Atualizando ambiente +wen.install.error=Ocorreu um erro durante a instalação de {} \ No newline at end of file diff --git a/bauh/gems/web/resources/locale/ru b/bauh/gems/web/resources/locale/ru index 01d65a8b..10b9dfbc 100644 --- a/bauh/gems/web/resources/locale/ru +++ b/bauh/gems/web/resources/locale/ru @@ -1,54 +1,12 @@ -gem.web.info=Позволяет устанавливать веб-приложения +gem.web.info=Позволяет устанавливать веб-приложения gem.web.install.warning=Это приложение официально не поддерживается владельцем домена -web.environment.install=Устанавливается {} -web.waiting.env_updater=Обнавляется Веб-среда +web.custom_action.clean_env=Очистка среды установки +web.custom_action.clean_env.failed=Произошла ошибка при очистке среды установки +web.custom_action.clean_env.status=Среда установки будет очищена +web.custom_action.clean_env.success=Среда установки очищена! web.env.checking=Проверка Веб-среды web.env.error=Проблемы с Веб-средой. Невозможно установить {}. -web.install.env_update.title=Обновление Веб-среды -web.install.env_update.body=Следующие файлы должны быть загружены и установлены -web.install.options.basic=базовые -web.install.options.advanced=расширенные -web.install.options_dialog.title=опции установки -web.install.error=Произошла ошибка во время установки {} -web.install.nativefier.error.unknown=Проверьте {} что-бы определить причину -web.install.nativefier.error.inner_dir=Каталог установки приложения не был найден в {} -web.install.substatus.call_nativefier=Работает {} -web.install.substatus.shortcut=Создается ярлык в меню -web.install.substatus.options=Ожидание опций установки -web.install.substatus.checking_fixes=Проверка опубликованных исправлений -web.install.option.single.label=НЕ запускать повтороно -web.install.option.single.tip=Эта опция не позволит приложению запуститься, если оно уже работает -web.install.option.max.label=Запускать развернутым -web.install.option.max.tip= Окно приложения запустится развернутым -web.install.option.fullscreen.label=Запускать во весь экран -web.install.option.fullscreen.tip=Приложение будет запущено во весь экран ( клавиша Alt открывает верхнее меню ) -web.install.option.noframe.label=Без рамки -web.install.option.noframe.tip=Приложение будет запущено без рамки окна -web.install.option.nocache.label=без кэша -web.install.option.nocache.tip=Данные сессии не будут сохранены после того, как приложение будет закрыто -web.install.option.insecure.label=Разрешить небезопасное содержимое -web.install.option.insecure.tip=Позволяет выполнять незащищенное содержимое внутри приложения -web.install.option.ignore_certificate.label=Игнорировать ошибки сертификата -web.install.option.ignore_certificate.tip=Ошибки, связанные с сертификатами, будут игнорироваться -web.install.option.allow_urls.label=Разрешить внутренние URL-адреса -web.install.option.allow_urls.tip=Позволяет приложению открывать внешние адреса / URL-адреса за пределами своей области. например: outlook.live.com сможет открыть account.microsoft.com -web.install.option.tray.label=Режим лотка -web.install.option.tray.off.label=Выкл -web.install.option.tray.off.tip=Выкл -web.install.option.tray.default.label=Запустить и прикрепить -web.install.option.tray.default.tip=Значок приложения будет прикреплен к панели задач после запуска -web.install.option.tray.min.label=Запускать свернутым -web.install.option.tray.min.tip=Приложение будет запущено свернутым в значок системного лотка -web.install.option.category.none=нет -web.install.option.icon.label=Пользовательский значок -web.install.option.wicon.label=Значок -web.install.option.wicon.deducted.label=Значек приложения -web.install.option.wicon.deducted.tip=Значек будет взят из {} в процессе установки -web.install.option.wicon.displayed.label=Отображать -web.install.option.wicon.displayed.tip=Значок, отображаемый на столе, будет использоваться для установки -web.install.global_nativefier.unavailable={n} не установлен в вашей системе, что не позволит установить {app} -web.uninstall.error.install_dir.not_found=Каталог установки {} не найден -web.uninstall.error.remove_dir=Не удалось удалить {} +web.environment.install=Устанавливается {} web.info.01_url=URL web.info.02_description=описание web.info.03_version=версия @@ -56,14 +14,62 @@ web.info.04_categories=категории web.info.05_installation_dir=каталог установки web.info.06_desktop_entry=ярлык web.info.07_exec_file=исполняемый -web.info.08_icon_path=значек +web.info.08_icon_path=значок web.info.09_size=размер web.info.10_config_dir=каталог конфигурационных файлов +web.install.env_update.body=Следующие файлы должны быть загружены и установлены +web.install.env_update.title=Обновление Веб-среды +web.install.error=Произошла ошибка во время установки {} +web.install.global_nativefier.unavailable={n} не установлен в вашей системе, что не позволит установить {app} +web.install.nativefier.error.inner_dir=Каталог установки приложения не был найден в {} +web.install.nativefier.error.unknown=Проверьте {} что-бы определить причину +web.install.option.allow_urls.label=Разрешить внутренние URL-адреса +web.install.option.allow_urls.tip=Позволяет приложению открывать внешние адреса / URL-адреса за пределами своей области. например: outlook.live.com сможет открыть account.microsoft.com +web.install.option.category.none=нет +web.install.option.fullscreen.label=Запускать во весь экран +web.install.option.fullscreen.tip=Приложение будет запущено во весь экран ( клавиша Alt открывает верхнее меню ) +web.install.option.icon.label=Пользовательский значок +web.install.option.ignore_certificate.label=Игнорировать ошибки сертификата +web.install.option.ignore_certificate.tip=Ошибки, связанные с сертификатами, будут игнорироваться +web.install.option.insecure.label=Разрешить небезопасное содержимое +web.install.option.insecure.tip=Позволяет выполнять незащищенное содержимое внутри приложения +web.install.option.max.label=Запускать развернутым +web.install.option.max.tip=Окно приложения запустится развернутым +web.install.option.nocache.label=без кэша +web.install.option.nocache.tip=Данные сессии не будут сохранены после того, как приложение будет закрыто +web.install.option.noframe.label=Без рамки +web.install.option.noframe.tip=Приложение будет запущено без рамки окна +web.install.option.single.label=Не запускать повтороно +web.install.option.single.tip=Эта опция не позволит приложению запуститься, если оно уже работает +web.install.option.tray.default.label=Запустить и прикрепить +web.install.option.tray.default.tip=Значок приложения будет прикреплен к панели задач после запуска +web.install.option.tray.label=Режим лотка +web.install.option.tray.min.label=Запускать свернутым +web.install.option.tray.min.tip=Приложение будет запущено свернутым в значок системного лотка +web.install.option.tray.off.label=Выкл +web.install.option.tray.off.tip=Выкл +web.install.option.wicon.deducted.label=Значок приложения +web.install.option.wicon.deducted.tip=Значок будет взят из {} в процессе установки +web.install.option.wicon.displayed.label=Отображать +web.install.option.wicon.displayed.tip=Значок, отображаемый на столе, будет использоваться для установки +web.install.option.wicon.label=Значок +web.install.options.advanced=расширенные +web.install.options.basic=базовые +web.install.options_dialog.title=опции установки +web.install.substatus.call_nativefier=Работает {} +web.install.substatus.checking_fixes=Проверка опубликованных исправлений +web.install.substatus.options=Ожидание опций установки +web.install.substatus.shortcut=Создается ярлык в меню web.settings.electron.version.label=версия Electron web.settings.electron.version.tooltip=Указывает альтернативный вариант Electron для визуализации устанавливаемых приложений -web.settings.nativefier.env=из Веб-среды -web.settings.nativefier.system=Из системы -web.settings.nativefier.env.tooltip= Будет использоваться версия Nativefier установленная в изолированную Веб-среду {app} -web.settings.nativefier.system.tooltip= Будет использоваться версия Nativefier установленная в Вашей системе -web.settings.nativefier.tip=Определяет, какие Nativefier версии должны быдут использованы для установки веб-приложений web.settings.env.nativefier.system.not_installed={} не установлен в Вашей системе +web.settings.nativefier.env=из Веб-среды +web.settings.nativefier.env.tooltip=Будет использоваться версия Nativefier установленная в изолированную Веб-среду {app} +web.settings.nativefier.system=Из системы +web.settings.nativefier.system.tooltip=Будет использоваться версия Nativefier установленная в Вашей системе +web.settings.nativefier.tip=Определяет, какие Nativefier версии должны быдут использованы для установки веб-приложений +web.task.download_settings=Updating environment settings +web.task.suggestions=Downloading applications suggestions list +web.uninstall.error.install_dir.not_found=Каталог установки {} не найден +web.uninstall.error.remove_dir=Не удалось удалить {} +web.waiting.env_updater=Обнавляется Веб-среда \ No newline at end of file diff --git a/bauh/gems/web/worker.py b/bauh/gems/web/worker.py index 48182222..87b1f33f 100644 --- a/bauh/gems/web/worker.py +++ b/bauh/gems/web/worker.py @@ -5,18 +5,31 @@ from pathlib import Path import requests import yaml +from bauh.api.abstract.handler import TaskManager from bauh.api.http import HttpClient -from bauh.gems.web import URL_SUGGESTIONS, TEMP_PATH, SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE +from bauh.gems.web import URL_SUGGESTIONS, TEMP_PATH, SEARCH_INDEX_FILE, SUGGESTIONS_CACHE_FILE, get_icon_path +from bauh.view.util.translation import I18n class SuggestionsDownloader: - def __init__(self, http_client: HttpClient, logger: logging.Logger): + def __init__(self, http_client: HttpClient, logger: logging.Logger, i18n: I18n, taskman: TaskManager = None): super(SuggestionsDownloader, self).__init__() self.http_client = http_client self.logger = logger + self.taskman = taskman + self.i18n = i18n + + def _finish_task(self): + if self.taskman: + self.taskman.update_progress('web_sugs', 100, None) + self.taskman.finish_task('web_sugs') def download(self) -> dict: + if self.taskman: + self.taskman.register_task('web_sugs', self.i18n['web.task.suggestions'], get_icon_path()) + self.taskman.update_progress('web_sugs', 10, None) + self.logger.info("Reading suggestions from {}".format(URL_SUGGESTIONS)) try: suggestions = self.http_client.get_yaml(URL_SUGGESTIONS, session=False) @@ -28,8 +41,10 @@ class SuggestionsDownloader: except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout): self.logger.warning("Internet seems to be off: it was not possible to retrieve the suggestions") + self._finish_task() return {} + self._finish_task() return suggestions diff --git a/bauh/manage.py b/bauh/manage.py new file mode 100644 index 00000000..28c71fe5 --- /dev/null +++ b/bauh/manage.py @@ -0,0 +1,75 @@ +import logging +from argparse import Namespace +from typing import Tuple + +from PyQt5.QtWidgets import QApplication, QWidget + +from bauh import ROOT_DIR, __app_name__ +from bauh.api.abstract.context import ApplicationContext +from bauh.api.http import HttpClient +from bauh.context import generate_i18n, DEFAULT_I18N_KEY, new_qt_application +from bauh.view.core import gems +from bauh.view.core.controller import GenericSoftwareManager +from bauh.view.core.downloader import AdaptableFileDownloader +from bauh.view.qt.prepare import PreparePanel +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.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) + icon_cache = cache_factory.new(int(app_config['memory_cache']['icon_expiration'])) + + http_client = HttpClient(logger) + + context = ApplicationContext(i18n=i18n, + http_client=http_client, + download_icons=bool(app_config['download']['icons']), + app_root_dir=ROOT_DIR, + cache_factory=cache_factory, + disk_loader_factory=DefaultDiskCacheLoaderFactory(logger), + logger=logger, + distro=util.get_distro(), + file_downloader=AdaptableFileDownloader(logger, bool(app_config['download']['multithreaded']), + i18n, http_client), + app_name=__app_name__) + + managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, default_locale=DEFAULT_I18N_KEY) + + if app_args.reset: + util.clean_app_files(managers) + exit(0) + + manager = GenericSoftwareManager(managers, context=context, config=app_config) + + app = new_qt_application(app_config, quit_on_last_closed=True) + + if app_args.settings: # only settings window + manager.prepare(None, None, None) # only checks the available managers + return app, SettingsWindow(manager=manager, i18n=i18n, screen_size=app.primaryScreen().size(), window=None) + else: + manage_window = ManageWindow(i18n=i18n, + manager=manager, + icon_cache=icon_cache, + screen_size=app.primaryScreen().size(), + config=app_config, + context=context, + http_client=http_client, + icon=util.get_default_icon()[1], + logger=logger) + + prepare = PreparePanel(screen_size=app.primaryScreen().size(), + context=context, + manager=manager, + i18n=i18n, + manage_window=manage_window) + cache_cleaner.start() + + return app, prepare diff --git a/bauh/tray.py b/bauh/tray.py new file mode 100644 index 00000000..6458dbfd --- /dev/null +++ b/bauh/tray.py @@ -0,0 +1,14 @@ +from typing import Tuple + +from PyQt5.QtWidgets import QApplication, QWidget + +from bauh.context import new_qt_application +from bauh.view.qt.systray import TrayIcon + + +def new_tray_icon(app_config: dict) -> Tuple[QApplication, QWidget]: + app = new_qt_application(app_config, quit_on_last_closed=True) + tray_icon = TrayIcon(screen_size=app.primaryScreen().size(), config=app_config) + tray_icon.show() + + return app, tray_icon diff --git a/bauh/view/core/config.py b/bauh/view/core/config.py index 4fe56fb5..60bc7f8c 100644 --- a/bauh/view/core/config.py +++ b/bauh/view/core/config.py @@ -1,6 +1,3 @@ -import logging -import os -import traceback from pathlib import Path import yaml @@ -21,17 +18,12 @@ def read_config(update_file: bool = False) -> dict: }, 'locale': None, 'updates': { - 'check_interval': 30, - 'sort_packages': True, - "pre_dependency_checking": True + 'check_interval': 30 }, 'system': { 'notifications': True, 'single_dependency_checking': False }, - 'disk_cache': { - 'enabled': True - }, 'suggestions': { 'enabled': True, 'by_type': 10 @@ -52,7 +44,20 @@ def read_config(update_file: bool = False) -> dict: 'download': { 'multithreaded': True, 'icons': True + }, + 'store_root_password': True, + 'disk': { + 'trim_after_update': False + }, + 'backup': { + 'enabled': True, + 'install': None, + 'uninstall': None, + 'downgrade': None, + 'upgrade': None, + 'mode': 'incremental' } + } return read(FILE_PATH, default, update_file=update_file, update_async=True) @@ -62,14 +67,3 @@ def save(config: dict): with open(FILE_PATH, 'w+') as f: f.write(yaml.safe_dump(config)) - - -def remove_old_config(logger: logging.Logger): - old_file = FILE_PATH.replace('.yml', '.json') - if os.path.exists(old_file): - try: - os.remove(old_file) - logger.info('Old configuration file {} deleted'.format(old_file)) - except: - logger.error('Could not delete the old configuration file {}'.format(old_file)) - traceback.print_exc() diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index 73a6dc47..0192f53b 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -1,21 +1,38 @@ +import os import re import time import traceback +from pathlib import Path from threading import Thread -from typing import List, Set, Type, Tuple +from typing import List, Set, Type, Tuple, Dict -from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext +from bauh import __app_name__, __version__ +from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ + UpgradeRequirement from bauh.api.abstract.disk import DiskCacheLoader -from bauh.api.abstract.handler import ProcessWatcher -from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction +from bauh.api.abstract.handler import ProcessWatcher, TaskManager +from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \ + CustomSoftwareAction from bauh.api.abstract.view import ViewComponent, TabGroupComponent +from bauh.api.constants import CACHE_PATH from bauh.api.exception import NoInternetException from bauh.commons import internet +from bauh.commons.html import bold, link from bauh.view.core.settings import GenericSettingsManager RE_IS_URL = re.compile(r'^https?://.+') +class GenericUpgradeRequirements(UpgradeRequirements): + + def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement], + to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[SoftwarePackage], + sub_requirements: Dict[SoftwareManager, UpgradeRequirements]): + super(GenericUpgradeRequirements, self).__init__(to_install=to_install, to_upgrade=to_upgrade, + to_remove=to_remove, cannot_upgrade=cannot_upgrade) + self.sub_requirements = sub_requirements + + class GenericSoftwareManager(SoftwareManager): def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict, @@ -32,6 +49,7 @@ class GenericSoftwareManager(SoftwareManager): self.working_managers = [] self.config = config self.settings_manager = settings_manager + self.http_client = context.http_client def reset_cache(self): if self._available_cache is not None: @@ -100,7 +118,7 @@ class GenericSoftwareManager(SoftwareManager): res = SearchResult([], [], 0) - if internet.is_available(self.context.http_client, self.context.logger): + if internet.is_available(): norm_word = word.strip().lower() url_words = RE_IS_URL.match(norm_word) @@ -142,26 +160,15 @@ class GenericSoftwareManager(SoftwareManager): def can_work(self) -> bool: return True - def _is_internet_available(self, res: dict): - res['available'] = internet.is_available(self.context.http_client, self.context.logger) - - def _get_internet_check(self, res: dict) -> Thread: - t = Thread(target=self._is_internet_available, args=(res,)) - t.start() - return t - - def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, net_check: bool = None) -> SearchResult: + 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() - net_check = {} - thread_internet_check = self._get_internet_check(net_check) - res = SearchResult([], None, 0) disk_loader = None - internet_available = None + net_available = internet.is_available() if not pkg_types: # any type for man in self.managers: if self._can_work(man): @@ -169,12 +176,8 @@ class GenericSoftwareManager(SoftwareManager): disk_loader = self.disk_loader_factory.new() disk_loader.start() - if internet_available is None: - thread_internet_check.join() - internet_available = net_check.get('available', True) - mti = time.time() - man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available) + man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_available) mtf = time.time() self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti)) @@ -191,12 +194,8 @@ class GenericSoftwareManager(SoftwareManager): disk_loader = self.disk_loader_factory.new() disk_loader.start() - if internet_available is None: - thread_internet_check.join() - internet_available = net_check.get('available', True) - mti = time.time() - man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available) + man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=net_available) mtf = time.time() self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(mtf - mti)) @@ -229,11 +228,14 @@ class GenericSoftwareManager(SoftwareManager): if man: return man.clean_cache_for(app) - def update(self, app: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: - man = self._get_manager_for(app) + def upgrade(self, requirements: GenericUpgradeRequirements, root_password: str, handler: ProcessWatcher) -> bool: + for man, man_reqs in requirements.sub_requirements.items(): + res = man.upgrade(man_reqs, root_password, handler) - if man: - return man.update(app, root_password, handler) + if not res: + return False + + return True def uninstall(self, app: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool: man = self._get_manager_for(app) @@ -283,56 +285,100 @@ class GenericSoftwareManager(SoftwareManager): return man if man and self._can_work(man) else None def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool): - if self.context.disk_cache and pkg.supports_disk_cache(): + if pkg.supports_disk_cache(): man = self._get_manager_for(pkg) if man: return man.cache_to_disk(pkg, icon_bytes=icon_bytes, only_icon=only_icon) - def requires_root(self, action: str, app: SoftwarePackage): - man = self._get_manager_for(app) + def requires_root(self, action: str, app: SoftwarePackage) -> bool: + if app is None: + if self.managers: + for man in self.managers: + if self._can_work(man): + if man.requires_root(action, app): + return True + return False + else: + man = self._get_manager_for(app) - if man: - return man.requires_root(action, app) + if man: + return man.requires_root(action, app) - def _prepare(self): + def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): if self.managers: + internet_on = internet.is_available() for man in self.managers: if man not in self._already_prepared and self._can_work(man): - man.prepare() + if task_manager: + man.prepare(task_manager, root_password, internet_on) self._already_prepared.append(man) - def prepare(self): - self.thread_prepare = Thread(target=self._prepare) - self.thread_prepare.start() - - def list_updates(self, net_check: bool = None) -> List[PackageUpdate]: + def list_updates(self, internet_available: bool = None) -> List[PackageUpdate]: self._wait_to_be_ready() updates = [] if self.managers: - net_check = {} - thread_internet_check = self._get_internet_check(net_check) + net_available = internet.is_available() for man in self.managers: if self._can_work(man): - - if thread_internet_check.is_alive(): - thread_internet_check.join() - - man_updates = man.list_updates(internet_available=net_check['available']) + man_updates = man.list_updates(internet_available=net_available) if man_updates: updates.extend(man_updates) return updates + def _check_for_updates(self) -> str: + self.logger.info("Checking for updates") + + try: + releases = self.http_client.get_json('https://api.github.com/repos/vinifmor/bauh/releases') + + if releases: + latest = None + + for r in releases: + if not r['draft']: + latest = r + break + + if latest and latest.get('tag_name'): + notifications_dir = '{}/updates'.format(CACHE_PATH) + release_file = '{}/{}'.format(notifications_dir, latest['tag_name']) + if os.path.exists(release_file): + self.logger.info("Release {} already notified".format(latest['tag_name'])) + elif latest['tag_name'] > __version__: + try: + Path(notifications_dir).mkdir(parents=True, exist_ok=True) + with open(release_file, 'w+') as f: + f.write('') + except: + self.logger.error("An error occurred while trying to create the update notification file: {}".format(release_file)) + + return self.i18n['warning.update_available'].format(bold(__app_name__), bold(latest['tag_name']), link(latest.get('html_url', '?'))) + else: + self.logger.info("No updates available") + else: + self.logger.warning("No official release found") + else: + self.logger.warning("No releases returned from the GitHub API") + except: + self.logger.error("An error occurred while trying to retrieve the current releases") + def list_warnings(self, internet_available: bool = None) -> List[str]: warnings = [] - if self.managers: - int_available = internet.is_available(self.context.http_client, self.context.logger) + int_available = internet.is_available() + if int_available: + updates_msg = self._check_for_updates() + + if updates_msg: + warnings.append(updates_msg) + + if self.managers: for man in self.managers: if man.is_enabled(): man_warnings = man.list_warnings(internet_available=int_available) @@ -360,7 +406,7 @@ class GenericSoftwareManager(SoftwareManager): def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: if bool(self.config['suggestions']['enabled']): - if self.managers and internet.is_available(self.context.http_client, self.context.logger): + if self.managers and internet.is_available(): suggestions, threads = [], [] for man in self.managers: t = Thread(target=self._fill_suggestions, args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed)) @@ -376,11 +422,11 @@ class GenericSoftwareManager(SoftwareManager): return suggestions return [] - def execute_custom_action(self, action: PackageAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher): - man = self._get_manager_for(pkg) + def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher): + man = action.manager if action.manager else self._get_manager_for(pkg) if man: - return exec('man.{}(pkg=pkg, root_password=root_password, watcher=watcher)'.format(action.manager_method)) + return eval('man.{}({}root_password=root_password, watcher=watcher)'.format(action.manager_method, 'pkg=pkg, ' if pkg else '')) def is_default_enabled(self) -> bool: return True @@ -418,9 +464,12 @@ class GenericSoftwareManager(SoftwareManager): def save_settings(self, component: TabGroupComponent) -> Tuple[bool, List[str]]: return self.settings_manager.save_settings(component) - def sort_update_order(self, pkgs: List[SoftwarePackage]) -> List[SoftwarePackage]: + def _map_pkgs_by_manager(self, pkgs: List[SoftwarePackage], pkg_filters: list = None) -> Dict[SoftwareManager, List[SoftwarePackage]]: by_manager = {} for pkg in pkgs: + if pkg_filters and not all((1 for f in pkg_filters if f(pkg))): + continue + man = self._get_manager_for(pkg) if man: @@ -432,42 +481,75 @@ class GenericSoftwareManager(SoftwareManager): man_pkgs.append(pkg) - sorted_list = [] + return by_manager - if by_manager: - for man, pkgs in by_manager.items(): - if len(pkgs) > 1: - ti = time.time() - sorted_list.extend(man.sort_update_order(pkgs)) - tf = time.time() - self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(tf - ti)) - else: - self.logger.info("Only one package to sort for {}. Ignoring sorting.".format(man.__class__.__name__)) - sorted_list.extend(pkgs) - - return sorted_list - - def get_update_requirements(self, pkgs: List[SoftwarePackage], watcher: ProcessWatcher) -> List[SoftwarePackage]: - by_manager = {} - for pkg in pkgs: - man = self._get_manager_for(pkg) - - if man: - man_pkgs = by_manager.get(man) - - if man_pkgs is None: - man_pkgs = [] - by_manager[man] = man_pkgs - - man_pkgs.append(pkg) - - required = [] + def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: str, watcher: ProcessWatcher) -> UpgradeRequirements: + by_manager = self._map_pkgs_by_manager(pkgs) + res = GenericUpgradeRequirements([], [], [], [], {}) if by_manager: for man, pkgs in by_manager.items(): ti = time.time() - required.extend(man.get_update_requirements(pkgs, watcher)) + man_reqs = man.get_upgrade_requirements(pkgs, root_password, watcher) tf = time.time() self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(tf - ti)) - return required + if not man_reqs: + return # it means the process should be stopped + + if man_reqs: + res.sub_requirements[man] = man_reqs + if man_reqs.to_install: + res.to_install.extend(man_reqs.to_install) + + if man_reqs.to_remove: + res.to_remove.extend(man_reqs.to_remove) + + if man_reqs.to_upgrade: + res.to_upgrade.extend(man_reqs.to_upgrade) + + if man_reqs.cannot_upgrade: + res.cannot_upgrade.extend(man_reqs.cannot_upgrade) + + return res + + def get_custom_actions(self) -> List[CustomSoftwareAction]: + if self.managers: + actions = [] + + working_managers = [] + + for man in self.managers: + if self._can_work(man): + working_managers.append(man) + + if working_managers: + working_managers.sort(key=lambda m: m.__class__.__name__) + + for man in working_managers: + man_actions = man.get_custom_actions() + + if man_actions: + actions.extend(man_actions) + + return actions + + def _fill_sizes(self, man: SoftwareManager, pkgs: List[SoftwarePackage]): + ti = time.time() + man.fill_sizes(pkgs) + tf = time.time() + self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(tf - ti)) + + def fill_sizes(self, pkgs: List[SoftwarePackage]): + by_manager = self._map_pkgs_by_manager(pkgs, pkg_filters=[lambda p: p.size is None]) + + if by_manager: + threads = [] + for man, man_pkgs in by_manager.items(): + if man_pkgs: + t = Thread(target=self._fill_sizes, args=(man, man_pkgs), daemon=True) + t.start() + threads.append(t) + + for t in threads: + t.join() diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py index 5f1c3775..b0bde420 100644 --- a/bauh/view/core/settings.py +++ b/bauh/view/core/settings.py @@ -11,7 +11,7 @@ from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.view import ViewComponent, TabComponent, InputOption, TextComponent, MultipleSelectComponent, \ PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \ FileChooserComponent -from bauh.view.core import config +from bauh.view.core import config, timeshift from bauh.view.core.config import read_config from bauh.view.util import translation from bauh.view.util.translation import I18n @@ -70,6 +70,11 @@ class GenericSettingsManager: tabs.append(self._gen_tray_settings(core_config, screen_width, screen_height)) tabs.append(self._gen_adv_settings(core_config, screen_width, screen_height)) + bkp_settings = self._gen_backup_settings(core_config, screen_width, screen_height) + + if bkp_settings: + tabs.append(bkp_settings) + for tab in gem_tabs: tabs.append(tab) @@ -78,12 +83,6 @@ class GenericSettingsManager: def _gen_adv_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: default_width = floor(0.11 * screen_width) - select_dcache = self._gen_bool_component(label=self.i18n['core.config.disk_cache'], - tooltip=self.i18n['core.config.disk_cache.tip'], - value=core_config['disk_cache']['enabled'], - max_width=default_width, - id_='dcache') - input_data_exp = TextInputComponent(label=self.i18n['core.config.mem_cache.data_exp'], tooltip=self.i18n['core.config.mem_cache.data_exp.tip'], value=str(core_config['memory_cache']['data_expiration']), @@ -98,17 +97,11 @@ class GenericSettingsManager: max_width=default_width, id_="icon_exp") - select_update_sort = self._gen_bool_component(label=self.i18n['core.config.updates.sort_pkgs'], - tooltip=self.i18n['core.config.updates.sort_pkgs.tip'], - value=bool(core_config['updates']['sort_packages']), - max_width=default_width, - id_="up_sort") - - select_update_check = self._gen_bool_component(label=self.i18n['core.config.updates.dep_check'], - tooltip=self.i18n['core.config.updates.dep_check.tip'], - value=bool(core_config['updates']['pre_dependency_checking']), - max_width=default_width, - id_="up_dep_check") + select_trim_up = self._gen_bool_component(label=self.i18n['core.config.trim_after_update'], + tooltip=self.i18n['core.config.trim_after_update.tip'], + value=bool(core_config['disk']['trim_after_update']), + max_width=default_width, + id_='trim_after_update') select_dep_check = self._gen_bool_component(label=self.i18n['core.config.system.dep_checking'], tooltip=self.i18n['core.config.system.dep_checking.tip'], @@ -122,7 +115,7 @@ class GenericSettingsManager: max_width=default_width, value=core_config['download']['multithreaded']) - sub_comps = [FormComponent([select_dcache, select_dmthread, select_update_check, select_update_sort, select_dep_check, input_data_exp, input_icon_exp], spaces=False)] + sub_comps = [FormComponent([select_dmthread, select_trim_up, select_dep_check, input_data_exp, input_icon_exp], spaces=False)] return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), PanelComponent(sub_comps), None, 'core.adv') def _gen_tray_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: @@ -219,6 +212,12 @@ class GenericSettingsManager: max_width=default_width, id_='locale') + select_store_pwd = self._gen_bool_component(label=self.i18n['core.config.store_password'].capitalize(), + tooltip=self.i18n['core.config.store_password.tip'].capitalize(), + id_="store_pwd", + max_width=default_width, + value=bool(core_config['store_root_password'])) + select_sysnotify = self._gen_bool_component(label=self.i18n['core.config.system.notifications'].capitalize(), tooltip=self.i18n['core.config.system.notifications.tip'].capitalize(), value=bool(core_config['system']['notifications']), @@ -238,7 +237,7 @@ class GenericSettingsManager: max_width=default_width, id_="sugs_by_type") - sub_comps = [FormComponent([select_locale, select_sysnotify, select_sugs, inp_sugs], spaces=False)] + sub_comps = [FormComponent([select_locale, select_store_pwd, select_sysnotify, select_sugs, inp_sugs], spaces=False)] return TabComponent(self.i18n['core.config.tab.general'].capitalize(), PanelComponent(sub_comps), None, 'core.gen') def _gen_bool_component(self, label: str, tooltip: str, value: bool, id_: str, max_width: int = 200) -> SingleSelectComponent: @@ -256,6 +255,7 @@ class GenericSettingsManager: def _save_settings(self, general: PanelComponent, advanced: PanelComponent, + backup: PanelComponent, ui: PanelComponent, tray: PanelComponent, gems_panel: PanelComponent) -> Tuple[bool, List[str]]: @@ -271,20 +271,17 @@ class GenericSettingsManager: core_config['system']['notifications'] = general_form.get_component('sys_notify').get_selected() core_config['suggestions']['enabled'] = general_form.get_component('sugs_enabled').get_selected() + core_config['store_root_password'] = general_form.get_component('store_pwd').get_selected() sugs_by_type = general_form.get_component('sugs_by_type').get_int_value() core_config['suggestions']['by_type'] = sugs_by_type # advanced adv_form = advanced.components[0] - core_config['disk_cache']['enabled'] = adv_form.get_component('dcache').get_selected() download_mthreaded = adv_form.get_component('down_mthread').get_selected() core_config['download']['multithreaded'] = download_mthreaded - core_config['updates']['pre_dependency_checking'] = adv_form.get_component('up_dep_check').get_selected() - core_config['updates']['sort_packages'] = adv_form.get_component('up_sort').get_selected() - single_dep_check = adv_form.get_component('dep_check').get_selected() core_config['system']['single_dependency_checking'] = single_dep_check @@ -294,6 +291,19 @@ class GenericSettingsManager: icon_exp = adv_form.get_component('icon_exp').get_int_value() core_config['memory_cache']['icon_expiration'] = icon_exp + core_config['disk']['trim_after_update'] = adv_form.get_component('trim_after_update').get_selected() + + # backup + if backup: + bkp_form = backup.components[0] + + core_config['backup']['enabled'] = bkp_form.get_component('enabled').get_selected() + core_config['backup']['mode'] = bkp_form.get_component('mode').get_selected() + core_config['backup']['install'] = bkp_form.get_component('install').get_selected() + core_config['backup']['uninstall'] = bkp_form.get_component('uninstall').get_selected() + core_config['backup']['upgrade'] = bkp_form.get_component('upgrade').get_selected() + core_config['backup']['downgrade'] = bkp_form.get_component('downgrade').get_selected() + # tray tray_form = tray.components[0] core_config['updates']['check_interval'] = tray_form.get_component('updates_interval').get_int_value() @@ -346,9 +356,11 @@ class GenericSettingsManager: saved, warnings = True, [] + bkp = component.get_tab('core.bkp') success, errors = self._save_settings(general=component.get_tab('core.gen').content, advanced=component.get_tab('core.adv').content, tray=component.get_tab('core.tray').content, + backup=bkp.content if bkp else None, ui=component.get_tab('core.ui').content, gems_panel=component.get_tab('core.types').content) @@ -378,3 +390,71 @@ class GenericSettingsManager: warnings.extend(errors) return saved, warnings + + def _gen_backup_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent: + if timeshift.is_available(): + default_width = floor(0.22 * screen_width) + + enabled_opt = self._gen_bool_component(label=self.i18n['core.config.backup'], + tooltip=None, + value=bool(core_config['backup']['enabled']), + id_='enabled', + max_width=default_width) + + ops_opts = [(self.i18n['yes'].capitalize(), True, None), + (self.i18n['no'].capitalize(), False, None), + (self.i18n['ask'].capitalize(), None, None)] + + install_mode = self._gen_select(label=self.i18n['core.config.backup.install'], + tip=None, + value=core_config['backup']['install'], + opts=ops_opts, + max_width=default_width, + id_='install') + + uninstall_mode = self._gen_select(label=self.i18n['core.config.backup.uninstall'], + tip=None, + value=core_config['backup']['uninstall'], + opts=ops_opts, + max_width=default_width, + id_='uninstall') + + upgrade_mode = self._gen_select(label=self.i18n['core.config.backup.upgrade'], + tip=None, + value=core_config['backup']['upgrade'], + opts=ops_opts, + max_width=default_width, + id_='upgrade') + + downgrade_mode = self._gen_select(label=self.i18n['core.config.backup.downgrade'], + tip=None, + value=core_config['backup']['downgrade'], + opts=ops_opts, + max_width=default_width, + id_='downgrade') + + mode = self._gen_select(label=self.i18n['core.config.backup.mode'], + tip=None, + value=core_config['backup']['mode'], + opts=[ + (self.i18n['core.config.backup.mode.incremental'], 'incremental', self.i18n['core.config.backup.mode.incremental.tip']), + (self.i18n['core.config.backup.mode.only_one'], 'only_one', self.i18n['core.config.backup.mode.only_one.tip']) + ], + max_width=default_width, + id_='mode') + + sub_comps = [FormComponent([enabled_opt, mode, install_mode, uninstall_mode, upgrade_mode, downgrade_mode], spaces=False)] + return TabComponent(self.i18n['core.config.tab.backup'].capitalize(), PanelComponent(sub_comps), None, 'core.bkp') + + def _gen_select(self, label: str, tip: str, id_: str, opts: List[tuple], value: str, max_width: int, type_: SelectViewType = SelectViewType.RADIO): + inp_opts = [InputOption(label=o[0].capitalize(), value=o[1], tooltip=o[2]) for o in opts] + def_opt = [o for o in inp_opts if o.value == value] + return SingleSelectComponent(label=label, + tooltip=tip, + options=inp_opts, + default_option=def_opt[0] if def_opt else inp_opts[0], + max_per_line=len(inp_opts), + max_width=max_width, + type_=type_, + id_=id_) + diff --git a/bauh/view/core/timeshift.py b/bauh/view/core/timeshift.py new file mode 100644 index 00000000..d7138db1 --- /dev/null +++ b/bauh/view/core/timeshift.py @@ -0,0 +1,13 @@ +from bauh.commons.system import run_cmd, SimpleProcess + + +def is_available() -> bool: + return bool(run_cmd('which timeshift', print_error=False)) + + +def delete_all_snapshots(root_password: str) -> SimpleProcess: + return SimpleProcess(['timeshift', '--delete-all', '--scripted'], root_password=root_password) + + +def create_snapshot(root_password: str) -> SimpleProcess: + return SimpleProcess(['timeshift', '--create', '--scripted'], root_password=root_password) diff --git a/bauh/view/core/tray_client.py b/bauh/view/core/tray_client.py new file mode 100644 index 00000000..340174b0 --- /dev/null +++ b/bauh/view/core/tray_client.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from bauh.api.constants import CACHE_PATH + +TRAY_CHECK_FILE = '{}/notify_tray'.format(CACHE_PATH) # it is a file that signals to the tray icon it should recheck for updates + + +def notify_tray(): + Path(CACHE_PATH).mkdir(exist_ok=True, parents=True) + + with open(TRAY_CHECK_FILE, 'w+') as f: + f.write('') diff --git a/bauh/view/qt/about.py b/bauh/view/qt/about.py index 5965a4cd..3e7d8cfb 100644 --- a/bauh/view/qt/about.py +++ b/bauh/view/qt/about.py @@ -1,12 +1,12 @@ from glob import glob from PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QPixmap, QIcon +from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel, QWidget, QHBoxLayout from bauh import __version__, __app_name__, ROOT_DIR -from bauh.view.util import resource, util -from bauh.view.util.translation import I18n +from bauh.context import generate_i18n +from bauh.view.util import resource PROJECT_URL = 'https://github.com/vinifmor/' + __app_name__ LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.format(__app_name__) @@ -14,14 +14,15 @@ LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.for class AboutDialog(QDialog): - def __init__(self, i18n: I18n): + def __init__(self, app_config: dict): super(AboutDialog, self).__init__() - self.setWindowTitle(i18n['tray.action.about']) + i18n = generate_i18n(app_config, resource.get_path('locale/about')) + self.setWindowTitle('{} ({})'.format(i18n['about.title'].capitalize(), __app_name__)) layout = QVBoxLayout() self.setLayout(layout) label_logo = QLabel() - icon = util.get_default_icon()[1].pixmap(64, 64) + icon = QIcon(resource.get_path('img/logo.svg')).pixmap(64, 64) label_logo.setPixmap(icon) label_logo.setAlignment(Qt.AlignCenter) layout.addWidget(label_logo) @@ -31,7 +32,7 @@ class AboutDialog(QDialog): label_name.setAlignment(Qt.AlignCenter) layout.addWidget(label_name) - label_version = QLabel(i18n['version'].lower() + ' ' + __version__) + label_version = QLabel(i18n['about.version'].lower() + ' ' + __version__) label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }') label_version.setAlignment(Qt.AlignCenter) layout.addWidget(label_version) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 60bbcaa8..1986c48e 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -15,7 +15,7 @@ from bauh.api.abstract.model import PackageStatus from bauh.commons.html import strip_html from bauh.view.qt import dialog from bauh.view.qt.components import IconButton -from bauh.view.qt.view_model import PackageView, PackageViewStatus +from bauh.view.qt.view_model import PackageView from bauh.view.util import resource from bauh.view.util.translation import I18n @@ -28,10 +28,10 @@ PUBLISHER_MAX_SIZE = 25 class UpdateToggleButton(QWidget): - def __init__(self, app_view: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True): + def __init__(self, pkg: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True): super(UpdateToggleButton, self).__init__() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) - self.app_view = app_view + self.app_view = pkg self.root = root layout = QHBoxLayout() @@ -40,22 +40,41 @@ class UpdateToggleButton(QWidget): self.setLayout(layout) self.bt = QToolButton() - self.bt.setCheckable(clickable) + self.bt.setCheckable(True) self.bt.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) if clickable: self.bt.clicked.connect(self.change_state) - self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg'))) self.bt.setStyleSheet('QToolButton { background: #20A435 } ' + - ('QToolButton:checked { background: gray }' if clickable else '')) - layout.addWidget(self.bt) + 'QToolButton:checked { background: gray } ' + + ('QToolButton:disabled { background: #d69003 }' if not clickable and not checked else '')) - self.setToolTip(i18n['manage_window.apps_table.upgrade_toggle.tooltip']) + layout.addWidget(self.bt) if not checked: self.bt.click() + if clickable: + self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg'))) + self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'], + i18n['manage_window.apps_table.upgrade_toggle.enabled.tooltip'])) + else: + if not checked: + self.bt.setIcon(QIcon(resource.get_path('img/exclamation.svg'))) + self.bt.setEnabled(False) + + tooltip = i18n['{}.update.disabled.tooltip'.format(pkg.model.gem_name)] + + if tooltip: + self.setToolTip(tooltip) + else: + self.setToolTip('{} {}'.format(i18n['manage_window.apps_table.upgrade_toggle.tooltip'], + i18n['manage_window.apps_table.upgrade_toggle.disabled.tooltip'])) + else: + self.bt.setIcon(QIcon(resource.get_path('img/app_update.svg'))) + self.bt.setCheckable(False) + def change_state(self, not_checked: bool): self.app_view.update_checked = not not_checked self.root.update_bt_upgrade() @@ -65,11 +84,10 @@ class AppsTable(QTableWidget): COL_NUMBER = 8 - def __init__(self, parent: QWidget, icon_cache: MemoryCache, disk_cache: bool, download_icons: bool): + def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool): super(AppsTable, self).__init__() self.setParent(parent) self.window = parent - self.disk_cache = disk_cache self.download_icons = download_icons self.setColumnCount(self.COL_NUMBER) self.setFocusPolicy(Qt.NoFocus) @@ -150,22 +168,13 @@ class AppsTable(QTableWidget): def refresh(self, pkg: PackageView): self._update_row(pkg, update_check_enabled=False, change_update_col=False) - def fill_async_data(self): - if self.window.pkgs: + def update_package(self, pkg: PackageView): + 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) - for idx, app_v in enumerate(self.window.pkgs): - - if app_v.status == PackageViewStatus.LOADING and app_v.model.status == PackageStatus.READY: - - if self.download_icons: - icon_request = QNetworkRequest(QUrl(app_v.model.icon_url)) - icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) - self.network_man.get(icon_request) - - self._update_row(app_v, change_update_col=False) - app_v.status = PackageViewStatus.READY - - self.window.resize_and_center() + self._update_row(pkg, change_update_col=False) def _uninstall_app(self, app_v: PackageView): if dialog.ask_confirmation(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'], @@ -222,18 +231,24 @@ class AppsTable(QTableWidget): col_name = self.item(idx, 0) col_name.setIcon(icon_data['icon']) - if self.disk_cache and app.model.supports_disk_cache() and app.model.get_disk_icon_path(): + if app.model.supports_disk_cache() and app.model.get_disk_icon_path(): 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'], only_icon=True) - def update_pkgs(self, pkg_views: List[PackageView], update_check_enabled: bool = True): - self.setRowCount(len(pkg_views) if pkg_views else 0) + def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = True): + self.setRowCount(len(pkgs) if pkgs else 0) self.setEnabled(True) - if pkg_views: - for idx, pkgv in enumerate(pkg_views): - pkgv.table_index = idx - self._update_row(pkgv, update_check_enabled) + if pkgs: + 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) + + self._update_row(pkg, update_check_enabled) def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, change_update_col: bool = True): self._set_col_name(0, pkg) @@ -250,7 +265,11 @@ class AppsTable(QTableWidget): if update_check_enabled and pkg.model.update: col_update = QToolBar() col_update.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - col_update.addWidget(UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked)) + col_update.addWidget(UpdateToggleButton(pkg=pkg, + root=self.window, + i18n=self.i18n, + checked=pkg.update_checked if pkg.model.can_be_updated() else False, + clickable=pkg.model.can_be_updated())) self.setCellWidget(pkg.table_index, 7, col_update) @@ -301,17 +320,18 @@ class AppsTable(QTableWidget): self.setCellWidget(pkg.table_index, col, toolbar) def _set_col_type(self, col: int, pkg: PackageView): + icon_data = self.cache_type_icon.get(pkg.model.get_type()) - pixmap = self.cache_type_icon.get(pkg.model.get_type()) - - if not pixmap: + if icon_data is None: pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(QSize(16, 16)) - self.cache_type_icon[pkg.model.get_type()] = pixmap + icon_data = {'px': pixmap, 'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())} + self.cache_type_icon[pkg.model.get_type()] = icon_data item = QLabel() - item.setPixmap(pixmap) + item.setPixmap(icon_data['px']) item.setAlignment(Qt.AlignCenter) - item.setToolTip('{}: {}'.format(self.i18n['type'], pkg.model.get_type().capitalize())) + + item.setToolTip(icon_data['tip']) self.setCellWidget(pkg.table_index, col, item) def _set_col_version(self, col: int, pkg: PackageView): @@ -343,8 +363,8 @@ class AppsTable(QTableWidget): item = QTableWidgetItem() item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - if pkg.model.name: - name = pkg.model.name + name = pkg.model.get_display_name() + if name: item.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), pkg.model.get_name_tooltip())) else: name = '...' @@ -359,7 +379,7 @@ class AppsTable(QTableWidget): item.setText(name) icon_path = pkg.model.get_disk_icon_path() - if self.disk_cache and pkg.model.supports_disk_cache() and icon_path and os.path.isfile(icon_path): + if pkg.model.supports_disk_cache() and icon_path and os.path.isfile(icon_path): with open(icon_path, 'rb') as f: icon_bytes = f.read() pixmap = QPixmap() diff --git a/bauh/view/qt/commons.py b/bauh/view/qt/commons.py index 4d652109..6bac8966 100644 --- a/bauh/view/qt/commons.py +++ b/bauh/view/qt/commons.py @@ -15,7 +15,7 @@ def new_pkgs_info() -> dict: def update_info(pkgv: PackageView, pkgs_info: dict): - pkgs_info['available_types'][pkgv.model.get_type()] = pkgv.model.get_type_icon_path() + 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(): pkgs_info['apps_count'] += 1 diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 338319dc..a980f761 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -1,13 +1,15 @@ import os +import time import traceback from pathlib import Path +from threading import Thread from typing import Tuple from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QIcon, QPixmap, QIntValidator from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \ - QSlider + QSlider, QScrollArea, QFrame from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ @@ -25,6 +27,9 @@ class RadioButtonQt(QRadioButton): self.model_parent = model_parent self.toggled.connect(self._set_checked) + if model.icon_path: + self.setIcon(QIcon(model.icon_path)) + if self.model.read_only: self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setFocusPolicy(Qt.NoFocus) @@ -90,7 +95,8 @@ class FormComboBoxQt(QComboBox): self.setMaximumWidth(model.max_width) for idx, op in enumerate(self.model.options): - self.addItem(op.label, op.value) + icon = QIcon(op.icon_path) if op.icon_path else QIcon() + self.addItem(icon, op.label, op.value) if op.tooltip: self.setItemData(idx, op.tooltip, Qt.ToolTipRole) @@ -178,7 +184,7 @@ class ComboSelectQt(QGroupBox): self.model = model self.setLayout(QGridLayout()) self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') - self.layout().addWidget(QLabel(model.label + ' :'), 0, 0) + self.layout().addWidget(QLabel(model.label + ' :' if model.label else ''), 0, 0) self.layout().addWidget(FormComboBoxQt(model), 0, 1) @@ -352,15 +358,27 @@ class InputFilter(QLineEdit): super(InputFilter, self).__init__() self.on_key_press = on_key_press self.last_text = '' + self.typing = None - def keyPressEvent(self, event): - super(InputFilter, self).keyPressEvent(event) + def notify_text_change(self): + time.sleep(2) text = self.text().strip() if text != self.last_text: self.last_text = text self.on_key_press() + self.typing = None + + def keyPressEvent(self, event): + super(InputFilter, self).keyPressEvent(event) + + if self.typing: + return + + self.typing = Thread(target=self.notify_text_change, daemon=True) + self.typing.start() + def get_text(self): return self.last_text @@ -464,11 +482,17 @@ class FormQt(QGroupBox): label_comp = QLabel() label.layout().addWidget(label_comp) + if hasattr(comp, 'size') and comp.size is not None: + label_comp.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }") + attr = 'label' if hasattr(comp,'label') else 'value' text = getattr(comp, attr) if text: - label_comp.setText(text.capitalize()) + if hasattr(comp, 'capitalize_label') and getattr(comp, 'capitalize_label'): + label_comp.setText(text.capitalize()) + else: + label_comp.setText(text) if comp.tooltip: label.layout().addWidget(self.gen_tip_icon(comp.tooltip)) @@ -499,7 +523,7 @@ class FormQt(QGroupBox): line_edit.setPlaceholderText(c.placeholder) if c.value: - line_edit.setText(c.value) + line_edit.setText(str(c.value) if c.value else '') line_edit.setCursorPosition(0) if c.read_only: @@ -604,7 +628,11 @@ class TabGroupQt(QTabWidget): traceback.print_exc() icon = QIcon() - self.addTab(to_widget(c.content, i18n), icon, c.label) + scroll = QScrollArea() + scroll.setFrameShape(QFrame.NoFrame) + scroll.setWidgetResizable(True) + scroll.setWidget(to_widget(c.content, i18n)) + self.addTab(scroll, icon, c.label) def new_single_select(model: SingleSelectComponent) -> QWidget: @@ -643,6 +671,10 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge return TwoStateButtonQt(comp) elif isinstance(comp, TextComponent): label = QLabel(comp.value) + + if comp.size is not None: + label.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }") + label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) return label elif isinstance(comp, SpacerComponent): diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index e3e7a440..29936cba 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -12,7 +12,7 @@ from bauh.view.util.translation import I18n class ConfirmationDialog(QMessageBox): def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, components: List[ViewComponent] = None, - confirmation_label: str = None, deny_label: str = None): + confirmation_label: str = None, deny_label: str = None, deny_button: bool = True): super(ConfirmationDialog, self).__init__() self.setWindowTitle(title) self.setStyleSheet('QLabel { margin-right: 25px; }') @@ -20,7 +20,8 @@ class ConfirmationDialog(QMessageBox): self.bt_yes.setStyleSheet(css.OK_BUTTON) self.setDefaultButton(self.bt_yes) - self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) + if deny_button: + self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) label = None if body: diff --git a/bauh/view/qt/gem_selector.py b/bauh/view/qt/gem_selector.py index 15f4b5a3..8cacff66 100644 --- a/bauh/view/qt/gem_selector.py +++ b/bauh/view/qt/gem_selector.py @@ -96,7 +96,7 @@ class GemSelectorPanel(QWidget): self.manager.prepare() self.window.verify_warnings() self.window.types_changed = True - self.window.refresh_apps() + self.window.refresh_packages() self.close() def exit(self): diff --git a/bauh/view/qt/history.py b/bauh/view/qt/history.py index 2498f2cc..aebd4cf6 100644 --- a/bauh/view/qt/history.py +++ b/bauh/view/qt/history.py @@ -7,6 +7,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.model import PackageHistory +from bauh.view.qt.view_model import get_type_label, PackageView from bauh.view.util.translation import I18n @@ -16,7 +17,9 @@ class HistoryDialog(QDialog): super(HistoryDialog, self).__init__() self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint) - self.setWindowTitle('{} - {} ({})'.format(i18n['popup.history.title'], history.pkg.name, history.pkg.get_type())) + view = PackageView(model=history.pkg, i18n=i18n) + + self.setWindowTitle('{} - {}'.format(i18n['popup.history.title'], view)) layout = QVBoxLayout() self.setLayout(layout) diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index b101df89..b91435f3 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -49,7 +49,7 @@ class InfoDialog(QDialog): for idx, attr in enumerate(sorted(app.keys())): if attr not in IGNORED_ATTRS and app[attr]: - i18n_key = app['__app__'].model.get_type().lower() + '.info.' + attr.lower() + i18n_key = app['__app__'].model.gem_name + '.info.' + attr.lower() if isinstance(app[attr], list): val = ' '.join([str(e).strip() for e in app[attr] if e]) diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py new file mode 100644 index 00000000..128630a8 --- /dev/null +++ b/bauh/view/qt/prepare.py @@ -0,0 +1,311 @@ +import datetime +import operator +import time +from functools import reduce +from typing import Tuple + +from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, QToolBar, \ + QProgressBar, QApplication + +from bauh.api.abstract.context import ApplicationContext +from bauh.api.abstract.controller import SoftwareManager +from bauh.api.abstract.handler import TaskManager +from bauh.view.qt import root +from bauh.view.qt.components import new_spacer +from bauh.view.qt.qt_utils import centralize +from bauh.view.qt.thread import AnimateProgress +from bauh.view.util.translation import I18n + + +class Prepare(QThread, TaskManager): + signal_register = pyqtSignal(str, str, object) + signal_update = pyqtSignal(str, float, str) + signal_finished = pyqtSignal(str) + signal_started = pyqtSignal() + signal_ask_password = pyqtSignal() + + def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n): + super(Prepare, self).__init__() + self.manager = manager + self.i18n = i18n + self.context = context + self.waiting_password = False + self.password_response = None + + def ask_password(self) -> Tuple[str, bool]: + self.waiting_password = True + self.signal_ask_password.emit() + + while self.waiting_password: + time.sleep(0.1) # waiting for user input + + return self.password_response + + def set_password_reply(self, password: str, valid: bool): + self.password_response = password, valid + self.waiting_password = False + + def run(self): + root_pwd = None + if self.manager.requires_root('prepare', None): + root_pwd, ok = self.ask_password() + + if not ok: + QCoreApplication.exit(1) + + self.signal_started.emit() + self.manager.prepare(self, root_pwd, None) + + def update_progress(self, task_id: str, progress: float, substatus: str): + self.signal_update.emit(task_id, progress, substatus) + + def register_task(self, id_: str, label: str, icon_path: str): + self.signal_register.emit(id_, label, icon_path) + + def finish_task(self, task_id: str): + self.signal_finished.emit(task_id) + + +class CheckFinished(QThread): + signal_finished = pyqtSignal() + + def __init__(self): + super(CheckFinished, self).__init__() + self.total = None + self.finished = None + + def run(self): + while True: + if self.total is not None and self.finished is not None: + if self.total == self.finished: + break + + time.sleep(0.01) + + self.signal_finished.emit() + + def update(self, total: int, finished: int): + if total is not None: + self.total = total + + if finished is not None: + self.finished = finished + + +class EnableSkip(QThread): + + signal_timeout = pyqtSignal() + + def run(self): + ti = datetime.datetime.now() + + while True: + if datetime.datetime.now() >= ti + datetime.timedelta(minutes=1.5): + self.signal_timeout.emit() + break + + time.sleep(0.1) + + +class PreparePanel(QWidget, TaskManager): + + signal_status = pyqtSignal(object, object) + signal_password_response = pyqtSignal(str, bool) + + def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen_size: QSize, i18n: I18n, manage_window: QWidget): + super(PreparePanel, self).__init__() + self.i18n = i18n + self.context = context + self.manage_window = manage_window + self.setWindowTitle(' ') + self.setMinimumWidth(screen_size.width() * 0.5) + self.setMinimumHeight(screen_size.height() * 0.35) + self.setMaximumHeight(screen_size.height() * 0.95) + self.setLayout(QVBoxLayout()) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + self.manager = manager + self.tasks = {} + self.ntasks = 0 + self.ftasks = 0 + self.self_close = False + + self.prepare_thread = Prepare(self.context, manager, self.i18n) + self.prepare_thread.signal_register.connect(self.register_task) + self.prepare_thread.signal_update.connect(self.update_progress) + self.prepare_thread.signal_finished.connect(self.finish_task) + self.prepare_thread.signal_started.connect(self.start) + self.prepare_thread.signal_ask_password.connect(self.ask_root_password) + self.signal_password_response.connect(self.prepare_thread.set_password_reply) + + self.check_thread = CheckFinished() + self.signal_status.connect(self.check_thread.update) + self.check_thread.signal_finished.connect(self.finish) + + self.skip_thread = EnableSkip() + self.skip_thread.signal_timeout.connect(self._enable_skip_button) + + self.progress_thread = AnimateProgress() + self.progress_thread.signal_change.connect(self._change_progress) + + self.label_top = QLabel() + self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize())) + self.label_top.setAlignment(Qt.AlignHCenter) + self.label_top.setStyleSheet("QLabel { font-size: 14px; font-weight: bold; }") + self.layout().addWidget(self.label_top) + self.layout().addWidget(QLabel()) + + self.table = QTableWidget() + self.table.setStyleSheet("QTableWidget { background-color: transparent; }") + self.table.setFocusPolicy(Qt.NoFocus) + self.table.setShowGrid(False) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setVisible(False) + self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(['' for _ in range(4)]) + self.layout().addWidget(self.table) + + toolbar = QToolBar() + self.bt_close = QPushButton(self.i18n['close'].capitalize()) + self.bt_close.clicked.connect(self.close) + self.bt_close.setVisible(False) + self.ref_bt_close = toolbar.addWidget(self.bt_close) + + toolbar.addWidget(new_spacer()) + self.progress_bar = QProgressBar() + self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 4) + self.progress_bar.setTextVisible(False) + self.progress_bar.setVisible(False) + self.ref_progress_bar = toolbar.addWidget(self.progress_bar) + toolbar.addWidget(new_spacer()) + + self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize()) + self.bt_skip.clicked.connect(self.finish) + self.bt_skip.setEnabled(False) + toolbar.addWidget(self.bt_skip) + + self.layout().addWidget(toolbar) + + def ask_root_password(self): + root_pwd, ok = root.ask_root_password(self.context, self.i18n) + self.signal_password_response.emit(root_pwd, ok) + + def _enable_skip_button(self): + self.bt_skip.setEnabled(True) + + def _change_progress(self, value: int): + self.progress_bar.setValue(value) + + def get_table_width(self) -> int: + return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())]) + + def _resize_columns(self): + header_horizontal = self.table.horizontalHeader() + for i in range(self.table.columnCount()): + header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents) + + self.resize(self.get_table_width() * 1.05, self.sizeHint().height()) + + def show(self): + super(PreparePanel, self).show() + self.prepare_thread.start() + centralize(self) + + def start(self): + self.ref_bt_close.setVisible(True) + self.check_thread.start() + self.skip_thread.start() + + self.ref_progress_bar.setVisible(True) + self.progress_thread.start() + + def closeEvent(self, QCloseEvent): + if not self.self_close: + QCoreApplication.exit() + + def register_task(self, id_: str, label: str, icon_path: str): + self.ntasks += 1 + self.table.setRowCount(self.ntasks) + + task_row = self.ntasks - 1 + + lb_icon = QLabel() + lb_icon.setContentsMargins(0, 2, 0, 0) + lb_icon.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + + if icon_path: + lb_icon.setPixmap(QIcon(icon_path).pixmap(12, 12)) + lb_icon.setAlignment(Qt.AlignHCenter) + + self.table.setCellWidget(task_row, 0, lb_icon) + + lb_status = QLabel(label) + lb_status.setAlignment(Qt.AlignHCenter) + lb_status.setContentsMargins(2, 0, 2, 0) + lb_status.setMinimumWidth(50) + lb_status.setAlignment(Qt.AlignHCenter) + lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_status.setStyleSheet("QLabel { color: blue; font-weight: bold; }") + self.table.setCellWidget(task_row, 1, lb_status) + + lb_sub = QLabel() + lb_sub.setContentsMargins(2, 0, 2, 0) + lb_sub.setAlignment(Qt.AlignHCenter) + lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_sub.setMinimumWidth(50) + self.table.setCellWidget(task_row, 2, lb_sub) + + lb_progress = QLabel('{0:.2f}'.format(0) + '%') + lb_progress.setContentsMargins(2, 2, 2, 2) + lb_progress.setStyleSheet("QLabel { color: blue; font-weight: bold; }") + lb_progress.setAlignment(Qt.AlignHCenter) + lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + + self.table.setCellWidget(task_row, 3, lb_progress) + + self.tasks[id_] = {'lb_status': lb_status, + 'lb_prog': lb_progress, + 'progress': 0, + 'lb_sub': lb_sub, + 'finished': False} + + self.signal_status.emit(self.ntasks, self.ftasks) + + def update_progress(self, task_id: str, progress: float, substatus: str): + task = self.tasks[task_id] + + if progress != task['progress']: + task['progress'] = progress + task['lb_prog'].setText('{0:.2f}'.format(progress) + '%') + + if substatus: + task['lb_sub'].setText('( {} )'.format(substatus)) + else: + task['lb_sub'].setText('') + + self._resize_columns() + + def finish_task(self, task_id: str): + task = self.tasks[task_id] + task['lb_sub'].setText('') + + for key in ('lb_prog', 'lb_status'): + task[key].setStyleSheet('QLabel { color: green; text-decoration: line-through; }') + + task['finished'] = True + self._resize_columns() + + self.ftasks += 1 + self.signal_status.emit(self.ntasks, self.ftasks) + + if self.ntasks == self.ftasks: + self.label_top.setText("... {} ...".format(self.i18n['ready'].capitalize())) + + def finish(self): + if self.isVisible(): + self.manage_window.refresh_packages() + self.manage_window.show() + self.self_close = True + self.close() diff --git a/bauh/view/qt/root.py b/bauh/view/qt/root.py index 539a1e89..bab883c5 100644 --- a/bauh/view/qt/root.py +++ b/bauh/view/qt/root.py @@ -1,9 +1,12 @@ import os +from typing import Tuple from PyQt5.QtWidgets import QInputDialog, QLineEdit +from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.view import MessageType from bauh.commons.system import new_subprocess +from bauh.view.core.config import read_config from bauh.view.qt.dialog import show_message from bauh.view.qt.view_utils import load_resource_icon from bauh.view.util.translation import I18n @@ -13,7 +16,14 @@ def is_root(): return os.getuid() == 0 -def ask_root_password(i18n: I18n): +def ask_root_password(context: ApplicationContext, i18n: I18n, app_config: dict = None) -> Tuple[str, bool]: + + cur_config = read_config() if not app_config else app_config + store_password = bool(cur_config['store_root_password']) + + if store_password and context.root_password and validate_password(context.root_password): + return context.root_password, True + diag = QInputDialog() diag.setStyleSheet("""QLineEdit { border-radius: 5px; font-size: 16px; border: 1px solid lightblue }""") diag.setInputMode(QInputDialog.TextInput) @@ -44,6 +54,9 @@ def ask_root_password(i18n: I18n): diag.setTextValue('') if ok: + if store_password: + context.root_password = diag.textValue() + return diag.textValue(), ok else: break diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py index 95093658..eeb29086 100644 --- a/bauh/view/qt/settings.py +++ b/bauh/view/qt/settings.py @@ -1,28 +1,29 @@ import gc from io import StringIO -from PyQt5.QtCore import QSize, Qt +from PyQt5.QtCore import QSize, Qt, QCoreApplication from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QPushButton +from bauh import __app_name__ from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.view import MessageType from bauh.view.core.controller import GenericSoftwareManager from bauh.view.qt import dialog, css from bauh.view.qt.components import to_widget, new_spacer +from bauh.view.qt.qt_utils import centralize from bauh.view.util import util from bauh.view.util.translation import I18n class SettingsWindow(QWidget): - def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, tray, window: QWidget, parent: QWidget = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, window: QWidget, parent: QWidget = None): super(SettingsWindow, self).__init__(parent=parent) - self.setWindowTitle(i18n['settings'].capitalize()) + self.setWindowTitle('{} ({})'.format(i18n['settings'].capitalize(), __app_name__)) self.setLayout(QVBoxLayout()) self.manager = manager self.i18n = i18n self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - self.tray = tray self.window = window self.settings_model = self.manager.get_settings(screen_size.width(), screen_size.height()) @@ -49,13 +50,16 @@ class SettingsWindow(QWidget): self.layout().addWidget(action_bar) + def show(self): + super(SettingsWindow, self).show() + centralize(self) + def closeEvent(self, event): if self.window and self.window.settings_window == self: self.deleteLater() self.window.settings_window = None - elif self.tray and self.tray.settings_window == self: - self.deleteLater() - self.tray.settings_window = None + elif not self.window: + QCoreApplication.exit() gc.collect() @@ -70,24 +74,29 @@ class SettingsWindow(QWidget): def _save_settings(self): success, warnings = self.manager.save_settings(self.settings_model) - # Configurações alteradas com sucesso, porém algumas delas só surtirão após a reinicialização - if success: - if dialog.ask_confirmation(title=self.i18n['warning'].capitalize(), - body="

{}

{}

".format(self.i18n['settings.changed.success.warning'], - self.i18n['settings.changed.success.reboot']), - i18n=self.i18n): - util.restart_app(self.window and self.window.isVisible()) + if not self.window: + dialog.show_message(title=self.i18n['success'].capitalize(), + body=self.i18n['settings.changed.success.warning'], + type_=MessageType.INFO) + QCoreApplication.exit() + elif dialog.ask_confirmation(title=self.i18n['warning'].capitalize(), + body="

{}

{}

".format(self.i18n['settings.changed.success.warning'], + self.i18n['settings.changed.success.reboot']), + i18n=self.i18n): + self.close() + util.restart_app() else: if isinstance(self.manager, GenericSoftwareManager): self.manager.reset_cache() - self.manager.prepare() + self.manager.prepare(task_manager=None, root_password=None, internet_available=None) if self.window and self.window.isVisible(): + self.window.update_custom_actions() self.window.verify_warnings() self.window.types_changed = True - self.window.refresh_apps() + self.window.refresh_packages() self.close() else: msg = StringIO() diff --git a/bauh/view/qt/systray.py b/bauh/view/qt/systray.py index c5f9dec7..8f6e0114 100755 --- a/bauh/view/qt/systray.py +++ b/bauh/view/qt/systray.py @@ -1,48 +1,82 @@ +import json +import os +import sys import time +import traceback from io import StringIO +from subprocess import Popen from threading import Lock, Thread from typing import List -from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt, QSize +from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, QSize from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QWidget, QSizePolicy +from PyQt5.QtWidgets import QSystemTrayIcon, QMenu -from bauh import __app_name__ -from bauh.api.abstract.controller import SoftwareManager +from bauh import __app_name__, ROOT_DIR from bauh.api.abstract.model import PackageUpdate -from bauh.view.qt import qt_utils +from bauh.commons.system import run_cmd +from bauh.context import generate_i18n +from bauh.view.core.tray_client import TRAY_CHECK_FILE from bauh.view.qt.about import AboutDialog -from bauh.view.qt.settings import SettingsWindow from bauh.view.qt.view_utils import load_resource_icon -from bauh.view.qt.window import ManageWindow -from bauh.view.util import util -from bauh.view.util.translation import I18n +from bauh.view.util import util, resource + + +def list_updates() -> List[PackageUpdate]: + if run_cmd('which bauh-cli', print_error=False): + output = run_cmd('bauh-cli updates -f json') + + if output: + return [PackageUpdate(pkg_id=o['id'], name=o['name'], version=o['version'], pkg_type=o['type']) for o in json.loads(output)] + + return [] class UpdateCheck(QThread): signal = pyqtSignal(list) - def __init__(self, manager: SoftwareManager, check_interval: int, parent=None): + def __init__(self, check_interval: int, lock: Lock, check_file: bool, parent=None): super(UpdateCheck, self).__init__(parent) self.check_interval = check_interval - self.manager = manager + self.lock = lock + self.check_file = check_file + + def _notify_updates(self): + self.lock.acquire() + try: + updates = list_updates() + + if updates is not None: + self.signal.emit(updates) + finally: + self.lock.release() + + time.sleep(self.check_interval) def run(self): - while True: - updates = self.manager.list_updates() - self.signal.emit(updates) - time.sleep(self.check_interval) + if self.check_file: + if os.path.exists(TRAY_CHECK_FILE): + self._notify_updates() + try: + os.remove(TRAY_CHECK_FILE) + except: + traceback.print_exc() + else: + time.sleep(self.check_interval) + else: + self._notify_updates() class TrayIcon(QSystemTrayIcon): - def __init__(self, i18n: I18n, manager: SoftwareManager, manage_window: ManageWindow, config: dict, screen_size: QSize): + def __init__(self, config: dict, screen_size: QSize, manage_process: Popen = None, settings_process: Popen = None): super(TrayIcon, self).__init__() - self.i18n = i18n - self.manager = manager + self.i18n = generate_i18n(config, resource.get_path('locale/tray')) self.screen_size = screen_size + self.manage_process = manage_process + self.settings_process = settings_process if config['ui']['tray']['default_icon']: self.icon_default = QIcon(config['ui']['tray']['default_icon']) @@ -67,7 +101,7 @@ class TrayIcon(QSystemTrayIcon): self.action_manage = self.menu.addAction(self.i18n['tray.action.manage']) self.action_manage.triggered.connect(self.show_manage_window) - self.action_settings = self.menu.addAction(self.i18n['settings'].capitalize()) + self.action_settings = self.menu.addAction(self.i18n['tray.settings'].capitalize()) self.action_settings.triggered.connect(self.show_settings_window) self.action_about = self.menu.addAction(self.i18n['tray.action.about']) @@ -82,10 +116,15 @@ class TrayIcon(QSystemTrayIcon): self.dialog_about = None self.settings_window = None - self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), manager=self.manager) + self.check_lock = Lock() + self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), check_file=False, lock=self.check_lock) self.check_thread.signal.connect(self.notify_updates) self.check_thread.start() + self.recheck_thread = UpdateCheck(check_interval=2, check_file=True, lock=self.check_lock) + self.recheck_thread.signal.connect(self.notify_updates) + self.recheck_thread.start() + self.last_updates = set() self.update_notification = bool(config['system']['notifications']) self.lock_notify = Lock() @@ -93,10 +132,8 @@ class TrayIcon(QSystemTrayIcon): self.activated.connect(self.handle_click) self.set_default_tooltip() - self.manage_window = manage_window - def set_default_tooltip(self): - self.setToolTip('{} ({})'.format(self.i18n['manage_window.title'], __app_name__).lower()) + self.setToolTip('{} ({})'.format(self.i18n['tray.action.manage'], __app_name__).lower()) def handle_click(self, reason): if reason == self.Trigger: @@ -109,7 +146,6 @@ class TrayIcon(QSystemTrayIcon): self.notify_updates(self.manager.list_updates(), notify_user=notify_user) def notify_updates(self, updates: List[PackageUpdate], notify_user: bool = True): - self.lock_notify.acquire() try: @@ -133,8 +169,8 @@ class TrayIcon(QSystemTrayIcon): msg.write(self.i18n['notification.update{}'.format('' if n_updates == 1 else 's')].format(n_updates)) if len(ups_by_type) > 1: - for ptype, count in ups_by_type.items(): - msg.write('\n * {} ( {} )'.format(ptype.capitalize(), count)) + for ptype in sorted(ups_by_type): + msg.write('\n * {} ( {} )'.format(ptype, ups_by_type[ptype])) msg.seek(0) msg = msg.read() @@ -155,25 +191,18 @@ class TrayIcon(QSystemTrayIcon): self.lock_notify.release() def show_manage_window(self): - if self.manage_window.isMinimized(): - self.manage_window.setWindowState(Qt.WindowNoState) - elif not self.manage_window.isVisible(): - self.manage_window.refresh_apps() - self.manage_window.show() + if self.manage_process is None: + self.manage_process = Popen([sys.executable, '{}/app.py'.format(ROOT_DIR)]) + elif self.manage_process.poll() is not None: # it means it has finished + self.manage_process = None + self.show_manage_window() def show_settings_window(self): - if self.settings_window: - self.settings_window.handle_display() - else: - self.settings_window = SettingsWindow(manager=self.manager, - i18n=self.i18n, - screen_size=self.screen_size, - tray=self, - window=self.manage_window) - self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4)) - self.settings_window.adjustSize() - qt_utils.centralize(self.settings_window) - self.settings_window.show() + if self.settings_process is None: + self.settings_process = Popen([sys.executable, '{}/app.py'.format(ROOT_DIR), '--settings']) + elif self.settings_process.poll() is not None: # it means it has finished + self.settings_process = None + self.show_settings_window() def show_about(self): if self.dialog_about is None: diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 6906ed70..99f67fe7 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -1,22 +1,30 @@ import os import re import time +import traceback from datetime import datetime, timedelta +from io import StringIO +from pathlib import Path from typing import List, Type, Set, Tuple import requests from PyQt5.QtCore import QThread, pyqtSignal +from bauh import LOGS_PATH from bauh.api.abstract.cache import MemoryCache -from bauh.api.abstract.controller import SoftwareManager +from bauh.api.abstract.controller import SoftwareManager, UpgradeRequirement, UpgradeRequirements from bauh.api.abstract.handler import ProcessWatcher -from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction -from bauh.api.abstract.view import InputViewComponent, MessageType, MultipleSelectComponent, InputOption +from bauh.api.abstract.model import PackageStatus, SoftwarePackage, CustomSoftwareAction +from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, TextComponent, \ + FormComponent, ViewComponent from bauh.api.exception import NoInternetException +from bauh.commons import user from bauh.commons.html import bold -from bauh.view.core import config +from bauh.commons.system import get_human_size_str, ProcessHandler, SimpleProcess +from bauh.view.core import timeshift +from bauh.view.core.config import read_config from bauh.view.qt import commons -from bauh.view.qt.view_model import PackageView +from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util.translation import I18n RE_VERSION_IN_NAME = re.compile(r'\s+version\s+[\w\.]+\s*$') @@ -32,6 +40,7 @@ class AsyncAction(QThread, ProcessWatcher): signal_substatus = pyqtSignal(str) # changes the GUI substatus message signal_progress = pyqtSignal(int) signal_root_password = pyqtSignal() + signal_progress_control = pyqtSignal(bool) def __init__(self): super(AsyncAction, self).__init__() @@ -40,9 +49,9 @@ class AsyncAction(QThread, ProcessWatcher): self.root_password = None self.stop = False - def request_confirmation(self, title: str, body: str, components: List[InputViewComponent] = None, confirmation_label: str = None, deny_label: str = None) -> bool: + def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None, deny_button: bool = True) -> bool: self.wait_confirmation = True - self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, 'confirmation_label': confirmation_label, 'deny_label': deny_label}) + self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, 'confirmation_label': confirmation_label, 'deny_label': deny_label, 'deny_button': deny_button}) self.wait_user() return self.confirmation_res @@ -90,117 +99,374 @@ class AsyncAction(QThread, ProcessWatcher): def should_stop(self): return self.stop + def enable_progress_controll(self): + self.signal_progress_control.emit(True) -class UpdateSelectedApps(AsyncAction): + def disable_progress_controll(self): + self.signal_progress_control.emit(False) - def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] = None): - super(UpdateSelectedApps, self).__init__() - self.pkgs = pkgs - self.manager = manager - self.root_password = None - self.i18n = i18n + def request_reboot(self, msg: str) -> bool: + if self.request_confirmation(title=self.i18n['action.request_reboot.title'], + body=msg, + confirmation_label=self.i18n['yes'].capitalize(), + deny_label=self.i18n['bt.not_now']): + ProcessHandler(self).handle_simple(SimpleProcess(['reboot'])) + return True - def _pkg_as_option(self, pkg: SoftwarePackage, tooltip: bool = True) -> InputOption: - if pkg.installed: - icon_path = pkg.get_disk_icon_path() + return False - if not icon_path or not os.path.isfile(icon_path): - icon_path = pkg.get_type_icon_path() - - else: - icon_path = pkg.get_type_icon_path() - - return InputOption(label='{}{}'.format(pkg.name, ' ( {} )'.format(pkg.latest_version) if pkg.latest_version else ''), - value=None, - tooltip=pkg.get_name_tooltip() if tooltip else None, - read_only=True, - icon_path=icon_path) - - def _handle_update_requirements(self, pkgs: List[SoftwarePackage]) -> bool: - self.change_substatus(self.i18n['action.update.requirements.status']) - required_pkgs = self.manager.get_update_requirements(pkgs, self) - - if required_pkgs: - opts = [self._pkg_as_option(p) for p in required_pkgs] - - if not self.request_confirmation(self.i18n['action.update.requirements.title'], - self.i18n['action.update.requirements.body'].format(bold(str(len(opts)))), - [MultipleSelectComponent(label='', options=opts, default_options=set(opts))], - confirmation_label=self.i18n['continue'].capitalize(), - deny_label=self.i18n['cancel'].capitalize()): - self.notify_finished({'success': False, 'updated': 0, 'types': set()}) - self.pkgs = None + def _generate_backup(self, app_config: dict, i18n: I18n, root_password: str) -> bool: + if timeshift.is_available(): + if app_config['backup']['mode'] not in ('only_one', 'incremental'): + self.show_message(title=self.i18n['error'].capitalize(), + body='{}: {}'.format(self.i18n['action.backup.invalid_mode'], bold(app_config['backup']['mode'])), + type_=MessageType.ERROR) return False + + if not user.is_root() and not root_password: + root_pwd, valid = self.request_root_password() else: - for pkg in required_pkgs: - if not self.manager.install(pkg, self.root_password, self): - self.notify_finished({'success': False, 'updated': 0, 'types': set()}) - self.pkgs = None - label = '{}{}'.format(pkg.name, ' ( {} )'.format(pkg.version) if pkg.version else '') - self.show_message(title=self.i18n['action.update.install_req.fail.title'], - body=self.i18n['action.update.install_req.fail.body'].format(label), - type_=MessageType.ERROR) - return False + root_pwd, valid = root_password, True + + if not valid: + return False + + handler = ProcessHandler(self) + if app_config['backup']['mode'] == 'only_one': + self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.delete'])) + deleted, _ = handler.handle_simple(timeshift.delete_all_snapshots(root_pwd)) + + if not deleted and not self.request_confirmation(title=i18n['core.config.tab.backup'], + body='{}. {}'.format(i18n['action.backup.error.delete'], i18n['action.backup.error.proceed']), + confirmation_label=i18n['yes'].capitalize(), + deny_label=i18n['no'].capitalize()): + return False + + self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.create'])) + created, _ = handler.handle_simple(timeshift.create_snapshot(root_pwd)) + + if not created and not self.request_confirmation(title=i18n['core.config.tab.backup'], + body='{}. {}'.format(i18n['action.backup.error.create'], + i18n['action.backup.error.proceed']), + confirmation_label=i18n['yes'].capitalize(), + deny_label=i18n['no'].capitalize()): + return False return True - def _sort_packages(self, pkgs: List[SoftwarePackage], app_config: dict) -> Tuple[bool, List[SoftwarePackage]]: - if bool(app_config['updates']['sort_packages']): - self.change_substatus(self.i18n['action.update.status.sorting']) - sorted_pkgs = self.manager.sort_update_order([view.model for view in self.pkgs]) - else: - sorted_pkgs = pkgs + def request_backup(self, app_config: dict, key: str, i18n: I18n, root_password: str = None) -> bool: + if bool(app_config['backup']['enabled']) and timeshift.is_available(): + val = app_config['backup'][key] if key else None - if len(sorted_pkgs) > 1: - opts = [self._pkg_as_option(p, tooltip=False) for p in sorted_pkgs] - proceed = self.request_confirmation(title=self.i18n['action.update.order.title'], - body=self.i18n['action.update.order.body'] + ':', - components=[MultipleSelectComponent(label='', options=opts, default_options=set(opts))], - confirmation_label=self.i18n['continue'].capitalize(), - deny_label=self.i18n['cancel'].capitalize()) - else: - proceed = True + if val is None: # ask mode + if self.request_confirmation(title=i18n['core.config.tab.backup'], + body=i18n['action.backup.msg'], + confirmation_label=i18n['yes'].capitalize(), + deny_label=i18n['no'].capitalize()): + res = self._generate_backup(app_config, i18n, root_password) + self.change_substatus('') + return res - return proceed, sorted_pkgs + elif val is True: # direct mode + res = self._generate_backup(app_config, i18n, root_password) + self.change_substatus('') + return res + + return True + + +class UpgradeSelected(AsyncAction): + + LOGS_DIR = '{}/upgrade'.format(LOGS_PATH) + SUMMARY_FILE = LOGS_DIR + '/{}_summary.txt' + + def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] = None): + super(UpgradeSelected, self).__init__() + self.pkgs = pkgs + self.manager = manager + self.i18n = i18n + + def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None) -> InputOption: + if req.pkg.installed: + icon_path = req.pkg.get_disk_icon_path() + + if not icon_path or not os.path.isfile(icon_path): + icon_path = req.pkg.get_type_icon_path() + + else: + icon_path = req.pkg.get_type_icon_path() + + size_str = '{}: {}'.format(self.i18n['size'].capitalize(), + '?' if req.extra_size is None else get_human_size_str(req.extra_size)) + if req.extra_size != req.required_size: + size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(), + '?' if req.required_size is None else get_human_size_str(req.required_size)) + + label = '{}{} - {}'.format(req.pkg.name, + ' ( {} )'.format(req.pkg.latest_version) if req.pkg.latest_version else '', + size_str) + + return InputOption(label=label, + value=None, + tooltip=custom_tooltip if custom_tooltip else (req.pkg.get_name_tooltip() if tooltip else None), + read_only=True, + icon_path=icon_path) + + def filter_to_update(self) -> Tuple[List[PackageView], bool]: # packages to update and if they require root privileges + to_update, requires_root = [], False + root_user = user.is_root() + + for pkg in self.pkgs: + if pkg.model.update and pkg.update_checked: + to_update.append(pkg) + + if not root_user and not requires_root and self.manager.requires_root('update', pkg.model): + requires_root = True + + return to_update, requires_root + + def _sum_pkgs_size(self, reqs: List[UpgradeRequirement]) -> Tuple[int, int]: + required, extra = 0, 0 + for r in reqs: + if r.required_size is not None: + required += r.required_size + + if r.extra_size is not None: + extra += r.extra_size + + return required, extra + + def _gen_cannot_update_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: + opts = [self._req_as_option(r.pkg, False, r.reason) for r in reqs] + comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] + + return FormComponent(label=self.i18n['action.update.cannot_update_label'], components=comps) + + def _gen_to_install_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]: + opts = [self._req_as_option(r) for r in reqs] + comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] + required_size, extra_size = self._sum_pkgs_size(reqs) + + lb = '{} ( {}: {}. {}: {}. {}: {} )'.format(self.i18n['action.update.required_label'].capitalize(), + self.i18n['amount'].capitalize(), + len(opts), + self.i18n['size'].capitalize(), + '?' if extra_size is None else get_human_size_str(extra_size), + self.i18n['action.update.pkg.required_size'].capitalize(), + '?' if required_size is None else get_human_size_str(required_size)) + return FormComponent(label=lb, components=comps), (required_size, extra_size) + + def _gen_to_remove_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: + opts = [self._req_as_option(r, False, r.reason) for r in reqs] + comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] + required_size, extra_size = self._sum_pkgs_size(reqs) + + lb = '{} ( {}: {}. {}: {} )'.format(self.i18n['action.update.label_to_remove'].capitalize(), + self.i18n['amount'].capitalize(), + len(opts), + self.i18n['size'].capitalize(), + '?' if extra_size is None else get_human_size_str(-extra_size)) + return FormComponent(label=lb, components=comps) + + def _gen_to_update_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]: + opts = [self._req_as_option(r, tooltip=False) for r in reqs] + comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] + required_size, extra_size = self._sum_pkgs_size(reqs) + + lb = '{} ( {}: {}. {}: {}. {}: {} )'.format(self.i18n['action.update.label_to_upgrade'].capitalize(), + self.i18n['amount'].capitalize(), + len(opts), + self.i18n['size'].capitalize(), + '?' if extra_size is None else get_human_size_str(extra_size), + self.i18n['action.update.pkg.required_size'].capitalize(), + '?' if required_size is None else get_human_size_str(required_size)) + + return FormComponent(label=lb, components=comps), (required_size, extra_size) + + def _trim_disk(self, root_password: str): + if self.request_confirmation(title=self.i18n['confirmation'].capitalize(), + body=self.i18n['action.trim_disk.ask']): + + pwd = root_password + + if not pwd and user.is_root(): + pwd, success = self.request_root_password() + + if not success: + return + + self.change_substatus(self.i18n['action.disk_trim'].capitalize()) + + success, output = ProcessHandler(self).handle_simple(SimpleProcess(['fstrim', '/', '-v'], root_password=pwd)) + + if not success: + self.show_message(title=self.i18n['success'].capitalize(), + body=self.i18n['action.disk_trim.error'], + type_=MessageType.ERROR) + + def _write_summary_log(self, upgrade_id: str, requirements: UpgradeRequirements): + try: + Path(self.LOGS_DIR).mkdir(parents=True, exist_ok=True) + + summary_text = StringIO() + summary_text.write('Upgrade summary ( id: {} )'.format(upgrade_id)) + + if requirements.cannot_upgrade: + summary_text.write('\nCannot upgrade:') + + for dep in requirements.cannot_upgrade: + type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize()) + summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label, dep.pkg.name, dep.pkg.version if dep.pkg.version else '?', dep.reason if dep.reason else '?')) + + summary_text.write('\n') + + if requirements.to_remove: + summary_text.write('\nMust be removed:') + + for dep in requirements.to_remove: + type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize()) + summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label, dep.pkg.name, dep.pkg.version if dep.pkg.version else '?', dep.reason if dep.reason else '?')) + + summary_text.write('\n') + + if requirements.to_install: + summary_text.write('\nMust be installed:') + + for dep in requirements.to_install: + type_label = self.i18n.get( + 'gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), + dep.pkg.get_type().capitalize()) + summary_text.write('\n * Type:{}\tName: {}\tVersion: {}\tReason: {}'.format(type_label, + dep.pkg.name, + dep.pkg.version if dep.pkg.version else '?', + dep.reason if dep.reason else '?')) + + summary_text.write('\n') + + if requirements.to_upgrade: + summary_text.write('\nWill be upgraded:') + + for dep in requirements.to_upgrade: + type_label = self.i18n.get('gem.{}.type.{}.label'.format(dep.pkg.gem_name, dep.pkg.get_type().lower()), dep.pkg.get_type().capitalize()) + summary_text.write( + '\n * Type:{}\tName: {}\tVersion:{} \tNew version: {}'.format(type_label, + dep.pkg.name, + dep.pkg.version if dep.pkg.version else '?', + dep.pkg.latest_version if dep.pkg.latest_version else '?')) + + summary_text.write('\n') + + summary_text.seek(0) + + with open(self.SUMMARY_FILE.format(upgrade_id), 'w+') as f: + f.write(summary_text.read()) + + except: + traceback.print_exc() def run(self): + to_update, requires_root = self.filter_to_update() + + root_password = None - success = False + if not user.is_root() and requires_root: + root_password, ok = self.request_root_password() - if self.pkgs: - updated, updated_types = 0, set() - - app_config = config.read_config() - - models = [view.model for view in self.pkgs] - - if bool(app_config['updates']['pre_dependency_checking']) and not self._handle_update_requirements(models): - return - - proceed, sorted_pkgs = self._sort_packages(models, app_config) - - if not proceed: - self.notify_finished({'success': success, 'updated': updated, 'types': updated_types}) + if not ok: + self.notify_finished({'success': False, 'updated': 0, 'types': set(), 'id': None}) self.pkgs = None return - for pkg in sorted_pkgs: - self.change_substatus('') - name = pkg.name if not RE_VERSION_IN_NAME.findall(pkg.name) else pkg.name.split('version')[0].strip() + if len(to_update) > 1: + self.disable_progress_controll() + else: + self.enable_progress_controll() - self.change_status('{} {} {}...'.format(self.i18n['manage_window.status.upgrading'], name, pkg.version)) - success = bool(self.manager.update(pkg, self.root_password, self)) - self.change_substatus('') + success = False - if not success: + updated, updated_types = 0, set() + + models = [view.model for view in to_update] + + self.change_substatus(self.i18n['action.update.requirements.status']) + requirements = self.manager.get_upgrade_requirements(models, root_password, self) + + if not requirements: + self.pkgs = None + self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None}) + return + + comps, required_size, extra_size = [], 0, 0 + + if requirements.cannot_upgrade: + comps.append(self._gen_cannot_update_form(requirements.cannot_upgrade)) + + if requirements.to_install: + req_form, reqs_size = self._gen_to_install_form(requirements.to_install) + required_size += reqs_size[0] + extra_size += reqs_size[1] + comps.append(req_form) + + if requirements.to_remove: + comps.append(self._gen_to_remove_form(requirements.to_remove)) + + updates_form, updates_size = self._gen_to_update_form(requirements.to_upgrade) + required_size += updates_size[0] + extra_size += updates_size[1] + comps.append(updates_form) + + extra_size_text = '{}: {}'.format(self.i18n['action.update.total_size'].capitalize(), get_human_size_str(extra_size)) + req_size_text = '{}: {}'.format(self.i18n['action.update.required_size'].capitalize(), + get_human_size_str(required_size)) + comps.insert(0, TextComponent(bold('{} | {}').format(extra_size_text, req_size_text), size=14)) + comps.insert(1, TextComponent('')) + + if not self.request_confirmation(title=self.i18n['action.update.summary'].capitalize(), body='', components=comps): + self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None}) + self.pkgs = None + return + + self.change_substatus('') + + app_config = read_config() + + if bool(app_config['backup']['enabled']) and app_config['backup']['upgrade'] in (True, None) and timeshift.is_available(): + any_requires_bkp = False + + for dep in requirements.to_upgrade: + if dep.pkg.supports_backup(): + any_requires_bkp = True break - else: - updated += 1 - updated_types.add(pkg.__class__) - self.signal_output.emit('\n') - self.notify_finished({'success': success, 'updated': updated, 'types': updated_types}) + if any_requires_bkp: + if not self.request_backup(app_config, 'upgrade', self.i18n, root_password): + self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None}) + self.pkgs = None + return + self.change_substatus('') + + timestamp = datetime.now() + upgrade_id = 'upgrade_{}{}{}_{}'.format(timestamp.year, timestamp.month, timestamp.day, int(time.time())) + + self._write_summary_log(upgrade_id, requirements) + + success = bool(self.manager.upgrade(requirements, root_password, self)) + self.change_substatus('') + + if success: + updated = len(requirements.to_upgrade) + updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade)) + + if bool(app_config['disk']['trim_after_update']): + self._trim_disk(root_password) + + msg = '

{}

{}


{}

'.format(self.i18n['action.update.success.reboot.line1'], + self.i18n['action.update.success.reboot.line2'], + self.i18n['action.update.success.reboot.line3']) + self.request_reboot(msg) + + self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': upgrade_id}) self.pkgs = None @@ -241,16 +507,24 @@ class RefreshApps(AsyncAction): class UninstallApp(AsyncAction): - def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, app: PackageView = None): + def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, app: PackageView = None): super(UninstallApp, self).__init__() self.app = app self.manager = manager self.icon_cache = icon_cache - self.root_password = None + self.root_pwd = None + self.i18n = i18n def run(self): if self.app: - success = self.manager.uninstall(self.app.model, self.root_password, self) + if self.app.model.supports_backup(): + if not self.request_backup(read_config(), 'uninstall', self.i18n, self.root_pwd): + self.notify_finished(False) + self.app = None + self.root_pwd = None + return + + success = self.manager.uninstall(self.app.model, self.root_pwd, self) if success: self.icon_cache.delete(self.app.model.icon_url) @@ -258,7 +532,7 @@ class UninstallApp(AsyncAction): self.notify_finished(self.app if success else None) self.app = None - self.root_password = None + self.root_pwd = None class DowngradeApp(AsyncAction): @@ -268,20 +542,28 @@ class DowngradeApp(AsyncAction): self.manager = manager self.app = app self.i18n = i18n - self.root_password = None + self.root_pwd = None def run(self): if self.app: success = False + + if self.app.model.supports_backup(): + if not self.request_backup(read_config(), 'downgrade', self.i18n, self.root_pwd): + self.notify_finished({'app': self.app, 'success': success}) + self.app = None + self.root_pwd = None + return + try: - success = self.manager.downgrade(self.app.model, self.root_password, self) + success = self.manager.downgrade(self.app.model, self.root_pwd, self) except (requests.exceptions.ConnectionError, NoInternetException) as e: success = False self.print(self.i18n['internet.required']) finally: self.notify_finished({'app': self.app, 'success': success}) self.app = None - self.root_password = None + self.root_pwd = None class GetAppInfo(AsyncAction): @@ -341,23 +623,27 @@ class SearchPackages(AsyncAction): class InstallPackage(AsyncAction): - def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None): + def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None): super(InstallPackage, self).__init__() self.pkg = pkg self.manager = manager self.icon_cache = icon_cache - self.disk_cache = disk_cache self.i18n = i18n - self.root_password = None + self.root_pwd = None def run(self): if self.pkg: success = False - try: - success = self.manager.install(self.pkg.model, self.root_password, self) + if self.pkg.model.supports_backup(): + if not self.request_backup(read_config(), 'install', self.i18n, self.root_pwd): + self.signal_finished.emit({'success': False, 'pkg': self.pkg}) + return - if success and self.disk_cache: + try: + success = self.manager.install(self.pkg.model, self.root_pwd, self) + + if success: self.pkg.model.installed = True if self.pkg.model.supports_disk_cache(): @@ -405,6 +691,9 @@ class AnimateProgress(QThread): self.increment = 0.5 self.paused = False + if self.progress_value >= val: + self.progress_value = val + def pause(self): self.paused = True @@ -440,47 +729,62 @@ class AnimateProgress(QThread): self._reset() -class VerifyModels(QThread): +class NotifyPackagesReady(QThread): - signal_updates = pyqtSignal() + signal_finished = pyqtSignal() + signal_changed = pyqtSignal(int) - def __init__(self, apps: List[PackageView] = None): - super(VerifyModels, self).__init__() - self.apps = apps + def __init__(self, pkgs: List[PackageView] = None): + super(NotifyPackagesReady, self).__init__() + self.pkgs = pkgs self.work = True def run(self): + timeout = datetime.now() + timedelta(seconds=15) - if self.apps: - - stop_at = datetime.utcnow() + timedelta(seconds=30) - last_ready = 0 - - while True: - + to_verify = {p.table_index: p for p in self.pkgs} + while self.work and datetime.now() < timeout: + to_remove = [] + for idx, pkg in to_verify.items(): if not self.work: break + elif pkg.model.status == PackageStatus.READY: + to_remove.append(idx) + if pkg.status == PackageViewStatus.LOADING: + self.signal_changed.emit(pkg.table_index) - current_ready = 0 + if not self.work: + break - for app in self.apps: - current_ready += 1 if app.model.status == PackageStatus.READY else 0 + if datetime.now() >= timeout: + break - if current_ready > last_ready: - last_ready = current_ready - self.signal_updates.emit() + for idx in to_remove: + del to_verify[idx] - if current_ready == len(self.apps): - self.signal_updates.emit() - break + if not to_verify: + break - if stop_at <= datetime.utcnow(): - break - - time.sleep(0.1) + time.sleep(0.1) + self.pkgs = None self.work = True - self.apps = None + self.signal_finished.emit() + + +class NotifyInstalledLoaded(QThread): + signal_loaded = pyqtSignal() + + def __init__(self): + super(NotifyInstalledLoaded, self).__init__() + self.loaded = False + + def notify_loaded(self): + self.loaded = True + + def run(self): + time.sleep(0.1) + self.signal_loaded.emit() class FindSuggestions(AsyncAction): @@ -560,30 +864,37 @@ class ApplyFilters(AsyncAction): class CustomAction(AsyncAction): - def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: PackageAction = None, pkg: PackageView = None, root_password: str = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: CustomSoftwareAction = None, pkg: PackageView = None, root_password: str = None): super(CustomAction, self).__init__() self.manager = manager self.pkg = pkg self.custom_action = custom_action - self.root_password = root_password + self.root_pwd = root_password self.i18n = i18n def run(self): - success = True - if self.pkg: - try: - success = self.manager.execute_custom_action(action=self.custom_action, - pkg=self.pkg.model, - root_password=self.root_password, - watcher=self) - except (requests.exceptions.ConnectionError, NoInternetException): - success = False - self.signal_output.emit(self.i18n['internet.required']) + if self.custom_action.backup: + app_config = read_config() + if not self.request_backup(app_config, None, self.i18n, self.root_pwd): + self.notify_finished({'success': False, 'pkg': self.pkg}) + self.pkg = None + self.custom_action = None + self.root_pwd = None + return + + try: + success = self.manager.execute_custom_action(action=self.custom_action, + pkg=self.pkg.model if self.pkg else None, + root_password=self.root_pwd, + watcher=self) + except (requests.exceptions.ConnectionError, NoInternetException): + success = False + self.signal_output.emit(self.i18n['internet.required']) self.notify_finished({'success': success, 'pkg': self.pkg}) self.pkg = None self.custom_action = None - self.root_password = None + self.root_pwd = None class GetScreenshots(AsyncAction): diff --git a/bauh/view/qt/view_model.py b/bauh/view/qt/view_model.py index cdac15b8..baa86de3 100644 --- a/bauh/view/qt/view_model.py +++ b/bauh/view/qt/view_model.py @@ -1,6 +1,7 @@ from enum import Enum -from bauh.api.abstract.model import SoftwarePackage +from bauh.api.abstract.model import SoftwarePackage, PackageStatus +from bauh.view.util.translation import I18n class PackageViewStatus(Enum): @@ -8,13 +9,22 @@ class PackageViewStatus(Enum): READY = 1 +def get_type_label(type_: str, gem: str, i18n: I18n) -> str: + type_label = 'gem.{}.type.{}.label'.format(gem, type_) + return i18n.get(type_label, type_.capitalize()).strip() + + class PackageView: - def __init__(self, model: SoftwarePackage): + def __init__(self, model: SoftwarePackage, i18n: I18n): self.model = model self.update_checked = model.update - self.status = PackageViewStatus.LOADING + self.status = PackageViewStatus.LOADING if model.status == PackageStatus.LOADING_DATA else PackageViewStatus.READY self.table_index = -1 + self.i18n = i18n + + def get_type_label(self) -> str: + return get_type_label(self.model.get_type(), self.model.gem_name, self.i18n) def __repr__(self): - return '{} ( {} )'.format(self.model.name, self.model.get_type()) + return '{} ( {} )'.format(self.model.name, self.get_type_label()) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index bb3fc206..cc72d826 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -1,22 +1,25 @@ import logging import time +import traceback from pathlib import Path -from typing import List, Type, Set +from typing import List, Type, Set, Tuple -from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal, QCoreApplication -from PyQt5.QtGui import QIcon, QWindowStateChangeEvent +from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \ - QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy + QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \ + QMenu, QAction +from bauh import LOGS_PATH from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager -from bauh.api.abstract.model import SoftwarePackage, PackageAction +from bauh.api.abstract.model import SoftwarePackage from bauh.api.abstract.view import MessageType from bauh.api.http import HttpClient from bauh.commons import user from bauh.commons.html import bold -from bauh.gems.web import TEMP_PATH +from bauh.view.core.tray_client import notify_tray from bauh.view.qt import dialog, commons, qt_utils, root from bauh.view.qt.about import AboutDialog from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton @@ -27,10 +30,11 @@ from bauh.view.qt.info import InfoDialog from bauh.view.qt.root import ask_root_password from bauh.view.qt.screenshots import ScreenshotsDialog from bauh.view.qt.settings import SettingsWindow -from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ - GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \ - AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots -from bauh.view.qt.view_model import PackageView +from bauh.view.qt.thread import UpgradeSelected, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ + GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, NotifyPackagesReady, FindSuggestions, \ + ListWarnings, \ + AsyncAction, LaunchApp, ApplyFilters, CustomSoftwareAction, GetScreenshots, CustomAction, NotifyInstalledLoaded +from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util import util, resource from bauh.view.util.translation import I18n @@ -49,12 +53,11 @@ class ManageWindow(QWidget): signal_table_update = pyqtSignal() def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size, config: dict, - context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon, tray_icon=None): + context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon): super(ManageWindow, self).__init__() self.i18n = i18n self.logger = logger self.manager = manager - self.tray_icon = tray_icon self.working = False # restrict the number of threaded actions self.pkgs = [] # packages current loaded in the table self.pkgs_available = [] # all packages loaded in memory @@ -188,7 +191,7 @@ class ManageWindow(QWidget): self.bt_installed.setToolTip(self.i18n['manage_window.bt.installed.tooltip']) self.bt_installed.setIcon(QIcon(resource.get_path('img/disk.svg'))) self.bt_installed.setText(self.i18n['manage_window.bt.installed.text'].capitalize()) - self.bt_installed.clicked.connect(self._show_installed) + self.bt_installed.clicked.connect(self._begin_loading_installed) self.bt_installed.setStyleSheet(toolbar_button_style('#A94E0A')) self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed) toolbar_bts.append(self.bt_installed) @@ -211,7 +214,7 @@ class ManageWindow(QWidget): self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg'))) self.bt_refresh.setText(self.i18n['manage_window.bt.refresh.text']) self.bt_refresh.setStyleSheet(toolbar_button_style('#2368AD')) - self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False)) + self.bt_refresh.clicked.connect(lambda: self.refresh_packages(keep_console=False)) toolbar_bts.append(self.bt_refresh) self.ref_bt_refresh = self.toolbar.addWidget(self.bt_refresh) @@ -238,9 +241,7 @@ class ManageWindow(QWidget): self.layout.addWidget(self.toolbar) - self.table_apps = AppsTable(self, self.icon_cache, - disk_cache=bool(self.config['disk_cache']['enabled']), - download_icons=bool(self.config['download']['icons'])) + self.table_apps = AppsTable(self, self.icon_cache, download_icons=bool(self.config['download']['icons'])) self.table_apps.change_headers_policy() self.layout.addWidget(self.table_apps) @@ -275,9 +276,9 @@ class ManageWindow(QWidget): self.layout.addWidget(self.toolbar_substatus) self._change_label_substatus('') - self.thread_update = self._bind_async_action(UpdateSelectedApps(self.manager, self.i18n), finished_call=self._finish_update_selected) + self.thread_update = self._bind_async_action(UpgradeSelected(self.manager, self.i18n), finished_call=self._finish_upgrade_selected) self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_apps, only_finished=True) - self.thread_uninstall = self._bind_async_action(UninstallApp(self.manager, self.icon_cache), finished_call=self._finish_uninstall) + self.thread_uninstall = self._bind_async_action(UninstallApp(self.manager, self.icon_cache, self.i18n), finished_call=self._finish_uninstall) self.thread_get_info = self._bind_async_action(GetAppInfo(self.manager), finished_call=self._finish_get_info) self.thread_get_history = self._bind_async_action(GetAppHistory(self.manager, self.i18n), finished_call=self._finish_get_history) self.thread_search = self._bind_async_action(SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True) @@ -292,14 +293,15 @@ class ManageWindow(QWidget): self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades) self.signal_table_update.connect(self.thread_apply_filters.stop_waiting) - self.thread_install = InstallPackage(manager=self.manager, disk_cache=bool(self.config['disk_cache']['enabled']), icon_cache=self.icon_cache, i18n=self.i18n) + self.thread_install = InstallPackage(manager=self.manager, icon_cache=self.icon_cache, i18n=self.i18n) self._bind_async_action(self.thread_install, finished_call=self._finish_install) self.thread_animate_progress = AnimateProgress() self.thread_animate_progress.signal_change.connect(self._update_progress) - self.thread_verify_models = VerifyModels() - self.thread_verify_models.signal_updates.connect(self._notify_model_data_change) + self.thread_notify_pkgs_ready = NotifyPackagesReady() + self.thread_notify_pkgs_ready.signal_changed.connect(self._update_package_data) + self.thread_notify_pkgs_ready.signal_finished.connect(self._update_state_when_pkgs_ready) self.toolbar_bottom = QToolBar() self.toolbar_bottom.setIconSize(QSize(16, 16)) @@ -315,6 +317,15 @@ class ManageWindow(QWidget): self.toolbar_bottom.addWidget(new_spacer()) + self.custom_actions = manager.get_custom_actions() + bt_custom_actions = IconButton(QIcon(resource.get_path('img/custom_actions.svg')), + action=self.show_custom_actions, + background="#669900", + i18n=self.i18n, + tooltip=self.i18n['manage_window.bt_custom_actions.tip']) + bt_custom_actions.setVisible(bool(self.custom_actions)) + self.ref_bt_custom_actions = self.toolbar_bottom.addWidget(bt_custom_actions) + bt_settings = IconButton(QIcon(resource.get_path('img/app_settings.svg')), action=self.show_settings, background='#12ABAB', @@ -351,15 +362,20 @@ class ManageWindow(QWidget): self.settings_window = None self.search_performed = False - def set_tray_icon(self, tray_icon): - self.tray_icon = tray_icon - # self.combo_styles.show_panel_after_restart = bool(tray_icon) + self.thread_load_installed = NotifyInstalledLoaded() + self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed) + + def update_custom_actions(self): + self.custom_actions = self.manager.get_custom_actions() + self.ref_bt_custom_actions.setVisible(bool(self.custom_actions)) def _update_process_progress(self, val: int): if self.progress_controll_enabled: self.thread_animate_progress.set_progress(val) def apply_filters_async(self): + self.thread_notify_pkgs_ready.work = False + self.thread_notify_pkgs_ready.wait(5) self.label_status.setText(self.i18n['manage_window.status.filtering'] + '...') self.ref_toolbar_search.setVisible(False) @@ -370,19 +386,32 @@ class ManageWindow(QWidget): self.thread_apply_filters.filters = self._gen_filters() self.thread_apply_filters.pkgs = self.pkgs_available self.thread_apply_filters.start() + self.checkbox_only_apps.setEnabled(False) + self.combo_categories.setEnabled(False) + self.combo_filter_type.setEnabled(False) + self.input_name_filter.setEnabled(False) + self.checkbox_updates.setEnabled(False) + self.table_apps.setEnabled(False) def _update_table_and_upgrades(self, pkgs_info: dict): self._update_table(pkgs_info=pkgs_info, signal=True) self.update_bt_upgrade(pkgs_info) - if self.pkgs_available: - self._notify_model_data_change() - self.thread_verify_models.work = False - self.thread_verify_models.wait(50) - self.thread_verify_models.apps = self.pkgs_available - self.thread_verify_models.start() + if self.pkgs: + self._update_state_when_pkgs_ready() + self.thread_notify_pkgs_ready.work = False + self.thread_notify_pkgs_ready.wait(5) + self.thread_notify_pkgs_ready.pkgs = self.pkgs + self.thread_notify_pkgs_ready.work = True + self.thread_notify_pkgs_ready.start() def _finish_apply_filters_async(self, success: bool): + self.checkbox_only_apps.setEnabled(True) + self.checkbox_updates.setEnabled(True) + self.combo_categories.setEnabled(True) + self.combo_filter_type.setEnabled(True) + self.input_name_filter.setEnabled(True) + self.table_apps.setEnabled(True) self.label_status.setText('') self.ref_toolbar_search.setVisible(True) @@ -399,7 +428,8 @@ class ManageWindow(QWidget): action.signal_status.connect(self._change_label_status) action.signal_substatus.connect(self._change_label_substatus) action.signal_progress.connect(self._update_process_progress) - action.signal_root_password.connect(self._ask_root_password) + action.signal_progress_control.connect(self.set_progress_controll) + action.signal_root_password.connect(self._pause_and_ask_root_password) self.signal_user_res.connect(action.confirm) self.signal_root_password.connect(action.set_root_password) @@ -414,14 +444,16 @@ class ManageWindow(QWidget): components=msg['components'], confirmation_label=msg['confirmation_label'], deny_label=msg['deny_label'], + deny_button=msg['deny_button'], screen_size=self.screen_size) res = diag.is_confirmed() self.thread_animate_progress.animate() self.signal_user_res.emit(res) - def _ask_root_password(self): + def _pause_and_ask_root_password(self): self.thread_animate_progress.pause() - password, valid = root.ask_root_password(self.i18n) + password, valid = root.ask_root_password(self.context, self.i18n) + self.thread_animate_progress.animate() self.signal_root_password.emit(password, valid) @@ -443,19 +475,23 @@ class ManageWindow(QWidget): def verify_warnings(self): self.thread_warnings.start() - def _show_installed(self): + def _begin_loading_installed(self): if self.pkgs_installed: - self.finish_action() self.search_performed = False self.ref_bt_upgrade.setVisible(True) self.ref_checkbox_only_apps.setVisible(True) self.input_search.setText('') self.input_name_filter.setText('') - self.update_pkgs(new_pkgs=None, as_installed=True) + self._begin_action(self.i18n['manage_window.status.installed'], keep_bt_installed=False, clear_filters=not self.recent_uninstall) + self.thread_load_installed.start() + + def _finish_loading_installed(self): + self.finish_action() + self.update_pkgs(new_pkgs=None, as_installed=True) def _show_about(self): if self.dialog_about is None: - self.dialog_about = AboutDialog(self.i18n) + self.dialog_about = AboutDialog(self.config) self.dialog_about.show() @@ -476,12 +512,20 @@ class ManageWindow(QWidget): self.category_filter = self.combo_categories.itemData(idx) self.apply_filters_async() - def _notify_model_data_change(self): - self.table_apps.fill_async_data() + def _update_state_when_pkgs_ready(self): + if self.progress_bar.isVisible(): + return if not self.recent_installation: - if not self.ref_progress_bar.isVisible(): - self._reload_categories() + self._reload_categories() + + self.resize_and_center() + + def _update_package_data(self, idx: int): + if self.table_apps.isEnabled(): + pkg = self.pkgs[idx] + pkg.status = PackageViewStatus.READY + self.table_apps.update_package(pkg) def _reload_categories(self): categories = set() @@ -500,14 +544,6 @@ class ManageWindow(QWidget): policy = QHeaderView.Stretch if self._maximized else QHeaderView.ResizeToContents self.table_apps.change_headers_policy(policy) - def closeEvent(self, event): - if self.tray_icon: - event.ignore() - self.hide() - self._handle_console_option(False) - else: - QCoreApplication.exit() # needed because QuitOnLastWindowClosed is disabled - def _handle_console(self, checked: bool): if checked: @@ -524,7 +560,7 @@ class ManageWindow(QWidget): self.checkbox_console.setChecked(False) self.textarea_output.hide() - def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None): + def refresh_packages(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None): self.recent_installation = False self.input_search.clear() @@ -561,20 +597,16 @@ class ManageWindow(QWidget): self._hide_fields_after_recent_installation() def uninstall_app(self, app: PackageView): - pwd = None - requires_root = self.manager.requires_root('uninstall', app.model) + pwd, proceed = self._ask_root_password('uninstall', app) - if not user.is_root() and requires_root: - pwd, ok = ask_root_password(self.i18n) - - if not ok: - return + if not proceed: + return self._handle_console_option(True) self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name), clear_filters=False) self.thread_uninstall.app = app - self.thread_uninstall.root_password = pwd + self.thread_uninstall.root_pwd = pwd self.thread_uninstall.start() def run_app(self, app: PackageView): @@ -595,10 +627,9 @@ class ManageWindow(QWidget): only_pkg_type = False self.recent_uninstall = True - self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None) + self.refresh_packages(pkg_types={pkgv.model.__class__} if only_pkg_type else None) - if self.tray_icon: - self.tray_icon.verify_updates() + notify_tray() else: if self._can_notify_user(): util.notify_user('{}: {}'.format(pkgv.model.name, self.i18n['notification.uninstall.failed'])) @@ -615,10 +646,9 @@ class ManageWindow(QWidget): if self._can_notify_user(): util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded'])) - self.refresh_apps(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) + self.refresh_packages(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None) - if self.tray_icon: - self.tray_icon.verify_updates(notify_user=False) + notify_tray() else: if self._can_notify_user(): util.notify_user(self.i18n['notification.downgrade.failed']) @@ -638,7 +668,7 @@ class ManageWindow(QWidget): def _update_table(self, pkgs_info: dict, signal: bool = False): self.pkgs = pkgs_info['pkgs_displayed'] - self.table_apps.update_pkgs(self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0) + self.table_apps.update_packages(self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0) if not self._maximized: self.table_apps.change_headers_policy(QHeaderView.Stretch) @@ -692,20 +722,20 @@ class ManageWindow(QWidget): setattr(self, attr, checked) checkbox.blockSignals(False) - def _gen_filters(self, updates: int = 0, ignore_updates: bool = False) -> dict: + 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_filter.get_text().lower() if self.input_name_filter.get_text() else None, - 'display_limit': self.display_limit if updates <= 0 else None + 'display_limit': None if self.filter_updates else self.display_limit } def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False): self.input_name_filter.setText('') pkgs_info = commons.new_pkgs_info() - filters = self._gen_filters(ignore_updates) + filters = self._gen_filters(ignore_updates=ignore_updates) if new_pkgs is not None: old_installed = None @@ -715,7 +745,7 @@ class ManageWindow(QWidget): self.pkgs_installed = [] for pkg in new_pkgs: - app_model = PackageView(model=pkg) + app_model = PackageView(model=pkg, i18n=self.i18n) commons.update_info(app_model, pkgs_info) commons.apply_filters(app_model, filters, pkgs_info) @@ -764,8 +794,8 @@ class ManageWindow(QWidget): self._update_table(pkgs_info=pkgs_info) if new_pkgs: - self.thread_verify_models.apps = self.pkgs - self.thread_verify_models.start() + self.thread_notify_pkgs_ready.pkgs = self.pkgs + self.thread_notify_pkgs_ready.start() if self.pkgs_installed: self.ref_bt_installed.setVisible(not as_installed and not self.recent_installation) @@ -774,7 +804,7 @@ class ManageWindow(QWidget): def _apply_filters(self, pkgs_info: dict, ignore_updates: bool): pkgs_info['pkgs_displayed'] = [] - filters = self._gen_filters(updates=pkgs_info['updates'], ignore_updates=ignore_updates) + filters = self._gen_filters(ignore_updates=ignore_updates) for pkgv in pkgs_info['pkgs']: commons.apply_filters(pkgv, filters, pkgs_info) @@ -795,7 +825,7 @@ class ManageWindow(QWidget): sel_type = -1 for idx, item in enumerate(available_types.items()): - app_type, icon_path = item[0], item[1] + app_type, icon_path, label = item[0], item[1]['icon'], item[1]['label'] icon = self.cache_type_filter_icons.get(app_type) @@ -803,7 +833,7 @@ class ManageWindow(QWidget): icon = QIcon(icon_path) self.cache_type_filter_icons[app_type] = icon - self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type) + self.combo_filter_type.addItem(icon, label, app_type) if keeping_selected and app_type == self.type_filter: sel_type = idx + 1 @@ -816,7 +846,6 @@ class ManageWindow(QWidget): self.ref_combo_filter_type.setVisible(False) def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False): - if categories is None: self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) else: @@ -835,9 +864,8 @@ class ManageWindow(QWidget): cat_list.sort() for idx, c in enumerate(cat_list): - i18n_cat = self.i18n.get(c) - cat_label = i18n_cat if i18n_cat else c - self.combo_categories.addItem(cat_label.capitalize(), c) + i18n_cat = self.i18n.get('category.{}'.format(c), self.i18n.get(c, c)) + self.combo_categories.addItem(i18n_cat.capitalize(), c) if keeping_selected and c == self.category_filter: selected_cat = idx + 1 @@ -863,49 +891,45 @@ class ManageWindow(QWidget): qt_utils.centralize(self) + def set_progress_controll(self, enabled: bool): + self.progress_controll_enabled = enumerate + def update_selected(self): - if self.pkgs: - requires_root = False + if dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'], + body=self.i18n['manage_window.upgrade_all.popup.body'], + i18n=self.i18n, + widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]): - to_update = [] + self._handle_console_option(True) + self._begin_action(self.i18n['manage_window.status.upgrading']) + self.thread_update.pkgs = self.pkgs + self.thread_update.start() - for app_v in self.pkgs: - if app_v.update_checked: - to_update.append(app_v) - - if self.manager.requires_root('update', app_v.model): - requires_root = True - - if to_update and dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'], - body=self.i18n['manage_window.upgrade_all.popup.body'], - i18n=self.i18n, - widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]): - pwd = None - - if not user.is_root() and requires_root: - pwd, ok = ask_root_password(self.i18n) - - if not ok: - return - - self._handle_console_option(True) - self.progress_controll_enabled = len(to_update) == 1 - self._begin_action(self.i18n['manage_window.status.upgrading']) - self.thread_update.pkgs = to_update - self.thread_update.root_password = pwd - self.thread_update.start() - - def _finish_update_selected(self, res: dict): + def _finish_upgrade_selected(self, res: dict): self.finish_action() + if res.get('id'): + output = self.textarea_output.toPlainText() + + if output: + try: + Path(UpgradeSelected.LOGS_DIR).mkdir(parents=True, exist_ok=True) + logs_path = '{}/{}.log'.format(UpgradeSelected.LOGS_DIR, res['id']) + with open(logs_path, 'w+') as f: + f.write(output) + + self.textarea_output.appendPlainText('\n*Upgrade summary generated at: {}'.format(UpgradeSelected.SUMMARY_FILE.format(res['id']))) + self.textarea_output.appendPlainText('*Upgrade logs generated at: {}'.format(logs_path)) + except: + traceback.print_exc() + if res['success']: if self._can_notify_user(): util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success'])) - self.refresh_apps(pkg_types=res['types']) + self.refresh_packages(pkg_types=res['types']) - if self.tray_icon: - self.tray_icon.verify_updates() + notify_tray() else: if self._can_notify_user(): util.notify_user(self.i18n['notification.update_selected.failed']) @@ -920,6 +944,7 @@ class ManageWindow(QWidget): self.ref_input_name_filter.setVisible(False) self.ref_combo_filter_type.setVisible(False) self.ref_combo_categories.setVisible(False) + self.ref_bt_custom_actions.setVisible(False) self.ref_bt_settings.setVisible(False) self.ref_bt_about.setVisible(False) self.thread_animate_progress.stop = False @@ -961,6 +986,7 @@ class ManageWindow(QWidget): self.progress_bar.setTextVisible(False) self._change_label_substatus('') + self.ref_bt_custom_actions.setVisible(bool(self.custom_actions)) self.ref_bt_settings.setVisible(True) self.ref_bt_about.setVisible(True) @@ -997,20 +1023,16 @@ class ManageWindow(QWidget): self.ref_input_name_filter.setVisible(False) def downgrade(self, pkgv: PackageView): - pwd = None - requires_root = self.manager.requires_root('downgrade', pkgv.model) + pwd, proceed = self._ask_root_password('downgrade', pkgv) - if not user.is_root() and requires_root: - pwd, ok = ask_root_password(self.i18n) - - if not ok: - return + if not proceed: + return self._handle_console_option(True) self._begin_action('{} {}'.format(self.i18n['manage_window.status.downgrading'], pkgv.model.name)) self.thread_downgrade.app = pkgv - self.thread_downgrade.root_password = pwd + self.thread_downgrade.root_pwd = pwd self.thread_downgrade.start() def get_app_info(self, pkg: dict): @@ -1043,7 +1065,7 @@ class ManageWindow(QWidget): body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)), type_=MessageType.ERROR) - def get_app_history(self, app: dict): + def get_app_history(self, app: PackageView): self._handle_console_option(False) self._begin_action(self.i18n['manage_window.status.history']) @@ -1062,6 +1084,10 @@ class ManageWindow(QWidget): self._handle_console_option(True) self.textarea_output.appendPlainText(res['error']) self.checkbox_console.setChecked(True) + elif not res['history'].history: + dialog.show_message(title=self.i18n['action.history.no_history.title'], + body=self.i18n['action.history.no_history.body'].format(bold(res['history'].pkg.name)), + type_=MessageType.WARNING) else: dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n) dialog_history.exec_() @@ -1090,21 +1116,29 @@ class ManageWindow(QWidget): else: dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING) - def install(self, pkg: PackageView): + def _ask_root_password(self, action: str, pkg: PackageView) -> Tuple[str, bool]: pwd = None - requires_root = self.manager.requires_root('install', pkg.model) + requires_root = self.manager.requires_root(action, pkg.model) if not user.is_root() and requires_root: - pwd, ok = ask_root_password(self.i18n) + pwd, ok = ask_root_password(self.context, self.i18n) if not ok: - return + return pwd, False + + return pwd, True + + def install(self, pkg: PackageView): + pwd, proceed = self._ask_root_password('install', pkg) + + if not proceed: + return self._handle_console_option(True) self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name)) self.thread_install.pkg = pkg - self.thread_install.root_password = pwd + self.thread_install.root_pwd = pwd self.thread_install.start() def _finish_install(self, res: dict): @@ -1114,7 +1148,7 @@ class ManageWindow(QWidget): console_output = self.textarea_output.toPlainText() if console_output: - log_path = '{}/logs/install/{}/{}'.format(TEMP_PATH, res['pkg'].model.get_type(), res['pkg'].model.name) + log_path = '{}/install/{}/{}'.format(LOGS_PATH, res['pkg'].model.get_type(), res['pkg'].model.name) try: Path(log_path).mkdir(parents=True, exist_ok=True) @@ -1146,27 +1180,34 @@ class ManageWindow(QWidget): def _finish_run_app(self, success: bool): self.finish_action() - def execute_custom_action(self, pkg: PackageView, action: PackageAction): + def execute_custom_action(self, pkg: PackageView, action: CustomSoftwareAction): + + if pkg is None and not dialog.ask_confirmation(title=self.i18n['confirmation'].capitalize(), + body=self.i18n['custom_action.proceed_with'].capitalize().format('"{}"'.format(self.i18n[action.i18_label_key].capitalize())), + icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), + i18n=self.i18n): + return False + pwd = None if not user.is_root() and action.requires_root: - pwd, ok = ask_root_password(self.i18n) + pwd, ok = ask_root_password(self.context, self.i18n) if not ok: return self._handle_console_option(True) - self._begin_action('{} {}'.format(self.i18n[action.i18n_status_key], pkg.model.name)) + self._begin_action('{}{}'.format(self.i18n[action.i18n_status_key], ' {}'.format(pkg.model.name) if pkg else '')) self.thread_custom_action.pkg = pkg - self.thread_custom_action.root_password = pwd + self.thread_custom_action.root_pwd = pwd self.thread_custom_action.custom_action = action self.thread_custom_action.start() def _finish_custom_action(self, res: dict): self.finish_action() if res['success']: - self.refresh_apps(pkg_types={res['pkg'].model.__class__}) + self.refresh_packages(pkg_types={res['pkg'].model.__class__} if res['pkg'] else None) else: self.checkbox_console.setChecked(True) @@ -1174,9 +1215,24 @@ class ManageWindow(QWidget): if self.settings_window: self.settings_window.handle_display() else: - self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, self.tray_icon, self) + self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, self) self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4)) self.settings_window.resize(self.size()) self.settings_window.adjustSize() qt_utils.centralize(self.settings_window) self.settings_window.show() + + def _map_custom_action(self, action: CustomSoftwareAction) -> QAction: + custom_action = QAction(self.i18n[action.i18_label_key]) + custom_action.setIcon(QIcon(action.icon_path)) + custom_action.triggered.connect(lambda: self.execute_custom_action(None, action)) + return custom_action + + def show_custom_actions(self): + if self.custom_actions: + menu_row = QMenu() + actions = [self._map_custom_action(a) for a in self.custom_actions] + menu_row.addActions(actions) + menu_row.adjustSize() + menu_row.popup(QCursor.pos()) + menu_row.exec_() diff --git a/bauh/view/resources/img/custom_actions.svg b/bauh/view/resources/img/custom_actions.svg new file mode 100644 index 00000000..0cbba23b --- /dev/null +++ b/bauh/view/resources/img/custom_actions.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/bauh/view/resources/img/exclamation.svg b/bauh/view/resources/img/exclamation.svg old mode 100755 new mode 100644 index cc8952cd..fae3701d --- a/bauh/view/resources/img/exclamation.svg +++ b/bauh/view/resources/img/exclamation.svg @@ -1,5 +1,5 @@ - + image/svg+xml - + inkscape:current-layer="Capa_1" /> + id="g1599" + style="fill:#ffffff"> + + + + id="g1605" + style="fill:#ffffff"> + + + + id="g1607"> + id="g1609"> + id="g1611"> + id="g1613"> + id="g1615"> + id="g1617"> + id="g1619"> + id="g1621"> + id="g1623"> + id="g1625"> + id="g1627"> + id="g1629"> + id="g1631"> - - \ No newline at end of file + id="g1633"> + + + + \ No newline at end of file diff --git a/bauh/view/resources/img/logo.svg b/bauh/view/resources/img/logo.svg old mode 100755 new mode 100644 index e480a993..3fda8ffa --- a/bauh/view/resources/img/logo.svg +++ b/bauh/view/resources/img/logo.svg @@ -1,5 +1,5 @@ - + image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Layer 1 + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file + id="path5075" + d="m -162.96707,107.61384 h 4.0278 l 2.08834,2.09848 v 4.00754 l -2.08834,2.09847 h -4.0278 l -2.08832,-2.09847 v -4.00754 z" + style="fill:#0055d4;stroke:#002255;stroke-width:0.924375;stroke-miterlimit:4;stroke-dasharray:none" /> + + + + + + + + + + + + + + + + + + diff --git a/bauh/view/resources/img/logo_update.svg b/bauh/view/resources/img/logo_update.svg old mode 100755 new mode 100644 index 2391fa85..2623a3b7 --- a/bauh/view/resources/img/logo_update.svg +++ b/bauh/view/resources/img/logo_update.svg @@ -1,5 +1,5 @@ - + image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file + id="path5119-5-2-5" + d="m 44.03842,138.50041 c 50.09748,0 50.00623,0 50.00623,0" + style="fill:#2b1100;stroke:#2b1100;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauh/view/resources/locale/about/ca b/bauh/view/resources/locale/about/ca new file mode 100644 index 00000000..59c8bcf1 --- /dev/null +++ b/bauh/view/resources/locale/about/ca @@ -0,0 +1,9 @@ +about.title=Quant a +about.info.desc=Interfície gràfica per a gestionar les vostres aplicacions per a Linux +about.info.link=Trobareu més informació a +about.info.license=Llicència lliure +about.info.rate.question=Us agrada aquesta eina? +about.info.rate.answer=Poseu-li una estrella al GitHub perquè el projecte sigui més fort +about.info.trouble.question=Problemes? +about.info.trouble.answer=Obriu un issue en GitHub perquè els desenvolupadors us puguin ajudar +about.version=versió \ No newline at end of file diff --git a/bauh/view/resources/locale/about/de b/bauh/view/resources/locale/about/de new file mode 100644 index 00000000..703db48e --- /dev/null +++ b/bauh/view/resources/locale/about/de @@ -0,0 +1,9 @@ +about.title=Über +about.info.desc=Grafisches Interface, um Linux-Anwendungen zu verwalten +about.info.link=Mehr Informationen unter +about.info.license=Freie Lizenz +about.info.rate.question=Gefällt Ihnen diese Anwendung? +about.info.rate.answer=Unterstützen Sie uns auf GitHub mit einem Stern. +about.info.trouble.question=Probleme? +about.info.trouble.answer=Eröffnen Sie ein GitHub-Issue, damit die Entwickler Ihnen helfen können. +about.version=Ausführung \ No newline at end of file diff --git a/bauh/view/resources/locale/about/en b/bauh/view/resources/locale/about/en new file mode 100644 index 00000000..074b2ec2 --- /dev/null +++ b/bauh/view/resources/locale/about/en @@ -0,0 +1,9 @@ +about.title=about +about.info.desc=Graphical interface for managing your Linux applications +about.info.link=More information at +about.info.license=Free license +about.info.rate.question=Like this tool ? +about.info.rate.answer=Give it a star on GitHub to strengthen the project +about.info.trouble.question=Any trouble ? +about.info.trouble.answer=Open a GitHub issue so the developers can help you +about.version=version \ No newline at end of file diff --git a/bauh/view/resources/locale/about/es b/bauh/view/resources/locale/about/es new file mode 100644 index 00000000..aad4d437 --- /dev/null +++ b/bauh/view/resources/locale/about/es @@ -0,0 +1,9 @@ +about.title=Acerca de +about.info.desc=Interfaz grafica para administración de aplicaciones para Linux +about.info.link=Mas información en +about.info.license=Licencia gratuita +about.info.rate.question=¿Le gusta esta herramienta? +about.info.rate.answer=Dele una estrella en GitHub para fortalecer el proyecto +about.info.trouble.question=¿Problemas? +about.info.trouble.answer=Abra un issue en GitHub para que los desarrolladores puedan ayudarlo +about.version=versión \ No newline at end of file diff --git a/bauh/view/resources/locale/about/it b/bauh/view/resources/locale/about/it new file mode 100644 index 00000000..3bb69db4 --- /dev/null +++ b/bauh/view/resources/locale/about/it @@ -0,0 +1,9 @@ +about.title=Informazioni su +about.info.desc=Interfaccia grafica per la gestione delle tue applicazioni Linux +about.info.link=Maggiori informazioni su +about.info.license=Free license +about.info.rate.question=Ti piace questo strumento? +about.info.rate.answer=Dagli una stella su GitHub per rafforzare il progetto +about.info.trouble.question=Problemas? +about.info.trouble.answer=Apri un issue in GitHub in modo che gli sviluppatori possano aiutarti +about.version=versione \ No newline at end of file diff --git a/bauh/view/resources/locale/about/pt b/bauh/view/resources/locale/about/pt new file mode 100644 index 00000000..d695c844 --- /dev/null +++ b/bauh/view/resources/locale/about/pt @@ -0,0 +1,9 @@ +about.title=sobre +about.info.desc=Interface gráfica para gerenciamento de aplicativos Linux +about.info.link=Mais informações em +about.info.license=Licença gratuita +about.info.rate.question=Gosta desta ferramenta ? +about.info.rate.answer=Dê a ela uma estrela no GitHub para fortalecer o projeto +about.info.trouble.question=Problemas ? +about.info.trouble.answer=Abra uma issue no GitHub para que os desenvolvedores possam ajudá-lo +about.version=versão \ No newline at end of file diff --git a/bauh/view/resources/locale/about/ru b/bauh/view/resources/locale/about/ru new file mode 100644 index 00000000..8ab1bc2f --- /dev/null +++ b/bauh/view/resources/locale/about/ru @@ -0,0 +1,9 @@ +about.title=О программе +about.info.desc=Графический интерфейс для управления приложениями Linux +about.info.link=Ещё информация на +about.info.license=Свободная лицензия +about.info.rate.question=Понравилось это приложеие ? +about.info.rate.answer=Поставьте ему звезду на GitHub, чтобы поддержать проект +about.info.trouble.question=Возникли проблемы ? +about.info.trouble.answer=Откройте GitHub, чтобы разработчики могли вам помочь +about.version=версия \ No newline at end of file diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 2bba847e..75cc15a4 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -1,281 +1,378 @@ -manage_window.title=les meves aplicacions -manage_window.columns.latest_version=Versió més recent -manage_window.columns.update=Voleu actualitzar? -manage_window.apps_table.row.actions.info=Informació -manage_window.apps_table.row.actions.history=Historial -manage_window.apps_table.row.actions.uninstall.popup.title=Desinstal·la -manage_window.apps_table.row.actions.uninstall.popup.body=Voleu desinstal·lar «{}» de l’ordinador? -manage_window.apps_table.row.actions.install.popup.title=Instal·lació -manage_window.apps_table.row.actions.install.popup.body=Voleu instal·lar «{}» a l’ordinador? -manage_window.apps_table.row.actions.downgrade=Reverteix -manage_window.apps_table.row.actions.downgrade.popup.body=Voleu revertir «{}»? -manage_window.apps_table.row.actions.install=Instal·la -manage_window.apps_table.row.actions.refresh=Refresca -manage_window.apps_table.upgrade_toggle.tooltip=Hi ha una actualització d’aquesta aplicació. Feu clic aquí per a marcar o desmarcar l’actualització -manage_window.checkbox.only_apps=Aplicacions -window_manage.input_search.placeholder=Cerca -window_manage.input_search.tooltip=Escriviu i premeu Intro per a cercar aplicacions -manage_window.label.updates=Actualitzacions -manage_window.status.refreshing=S’està refrescant -manage_window.status.upgrading=S’està actualitzant -manage_window.status.uninstalling=S’està desinstal·lant -manage_window.status.downgrading=S’està revertint -manage_window.status.info=S’està recuperant la informació -manage_window.status.history=S’està recuperant l’historial -manage_window.status.searching=S’està cercant -manage_window.status.installing=S’està instal·lant -manage_window.status.refreshing_app=S’està refrescant -manage_window.status.running_app=S’està iniciant «{}» -manage_window.status.filtering=S’està filtrant -manage_window.status.screenshots=S’estan recuperant {} captures de pantalla -manage_window.status.suggestions=S’estan buscant suggeriments -manage_window.bt.refresh.text=Refresca -manage_window.bt.refresh.tooltip=Carrega novament les dades quant a les aplicacions instal·lades -manage_window.bt.upgrade.text=Actualitza -manage_window.bt.upgrade.tooltip=Actualitza totes les aplicacions marcades -manage_window.bt.installed.text=instal·lada -manage_window.bt.installed.tooltip=Feu clic aquí per a mostrar les aplicacions instal·lades -manage_window.bt.suggestions.text=Suggeriments -manage_window.bt.suggestions.tooltip=Feu clic aquí per a mostrar alguns suggeriments d’aplicacions -manage_window.checkbox.show_details=Mostra detalls -popup.root.title=Requireix la vostra contrasenya per a continuar -popup.root.continue=continuar -popup.root.bad_password.title=Error -popup.root.bad_password.body=La contrasenya és incorrecta -popup.root.bad_password.last_try=S’han acabat els intents. S’ha cancel·lat l’operació. -popup.history.title=Historial -popup.history.selected.tooltip=Versió actual -popup.button.yes=Sí -popup.button.no=No -popup.button.cancel=Cancel·la -tray.action.manage=Aplicacions -tray.action.exit=Surt -tray.action.about=Quant a -tray.action.refreshing=Refrescant +Australia=Australia +Austria=Austria +Bangladesh=Bangladesh +Belarus=Belarus +Belgium=Belgium +Brazil=Brazil +Bulgaria=Bulgaria +Canada=Canada +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=Czech +Denmark=Denmark +Ecuador=Ecuador +France=France +Georgia=Georgia +Germany=Germany +Greece=Greece +Hong Kong=Hong Kong +Hungary=Hungary +Iceland=Iceland +India=India +Indonesia=Indonesia +Iran=Iran +Italy=Italy +Japan=Japan +Kenya=Kenya +Netherlands=Netherlands +New Zealand=New Zealand +Norway=Norway +Philippines=Philippines +Poland=Poland +Portugal=Portugal +Russia=Russia +Singapore=Singapore +South Africa=South Africa +South Korea=South Korea +Spain=Spain +Sweden=Sweden +Switzerland=Switzerland +Taiwan=Taiwan +Thailand=Thailand +Turkey=Turkey +Ukraine=Ukraine +United Kingdom=United Kingdom +United States=United States +action.backup.error.create=It was not possible to generate a new system copy +action.backup.error.delete=It was not possible to delete the old system copies +action.backup.error.proceed=Proceed anyway ? +action.backup.invalid_mode=Invalid backup mode +action.backup.msg=Do you want to generate a system copy before proceeding ? +action.backup.substatus.create=Generating a new system backup +action.backup.substatus.delete=Removing old system backups +action.cancelled=l’usuari ha cancel·lat l’operació +action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim.error=There was a problem while optimizing the disk +action.failed=L'acció ha fallat +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history action.info.tooltip=Feu clic aquí per a veure informació sobre aquesta aplicació +action.not_allowed=Action not allowed +action.request_reboot.title=System restart +action.run.tooltip=Feu clic aquí per a iniciar l’aplicació action.screenshots.tooltip=Feu clic aquí per a veure imatges d’aquesta aplicació action.settings.tooltip=Feu clic aquí per a obrir accions addicionals -notification.update={} actualització disponible -notification.updates={} actualitzacions disponibles -notification.update_selected.success=aplicacions actualitzades correctament -notification.update_selected.failed=Ha fallat l’actualització -notification.install.failed=ha fallat la instal·lació -notification.uninstall.failed=ha fallat la desinstal·lació -notification.downgrade.failed=Ha fallat la reversió -about.info.desc=Interfície gràfica per a gestionar les vostres aplicacions per a Linux -about.info.link=Trobareu més informació a -about.info.license=Llicència lliure -about.info.rate.question=Us agrada aquesta eina? -about.info.rate.answer=Poseu-li una estrella al GitHub perquè el projecte sigui més fort -about.info.trouble.question=Problemes? -about.info.trouble.answer=Obriu un issue en GitHub perquè els desenvolupadors us puguin ajudar -yes=sí -no=no -version.updated=actualitzada -version.outdated=no actualitzada -name=nom -version=versió -latest_version=versió més recent -description=descripció -type=tipus -installed=instal·lada -uninstalled=desinstal·lada -not_installed=no instal·lada -downgraded=revertida -others=altres -internet.required=Us cal una connexió a Internet -updates=actualitzacions -version.installed=versió instal·lada -version.latest=versió més recent -version.installed_outdated=la versió instal·lada no està actualitzada -version.unknown=no informat -app.name=nom de l’aplicació -warning=avís -install=instal·la -uninstall=desinstal·la -bt.app_upgrade=Actualita -bt.app_not_upgrade=No actualitzis -manage_window.upgrade_all.popup.title=Actualitza -manage_window.upgrade_all.popup.body=Voleu actualitzar totes les aplicacions seleccionades? -manage_window.settings.about=Quant a -confirmation=confirmació -and=i -error=error -action.cancelled=l’usuari ha cancel·lat l’operació -copy=copia -back=enrere -show=mostra -ask.continue=Voleu continuar? -cancel=cancel·la -status.caching_data=S’estan emmagatzemant {} dades a la memòria cau al disc -action.run.tooltip=Feu clic aquí per a iniciar l’aplicació -any=qualsevol -manage_window.name_filter.tooltip=Escriviu aquí per a filtrar aplicacions per nom -manage_window.name_filter.placeholder=Filtra per nom -publisher=proveïdor -unknown=desconegut -welcome=us donem la benvinguda -gem_selector.title=Tipus d’aplicacions -gem_selector.question=Quins tipus d’aplicacions voleu que es mostrin aquí? -proceed=continua -change=modifica -exit=surt -style=estil -manage_window.bt_settings.tooltip=Feu clic aquí per mostrar la configuració -downloading=S’està baixant -console.install_logs.path=Trobareu registres d’instal·lació a {} -author=autor -source=font -size=mida -category=categoria -categories=categories -summary=resum -popup.screenshots.no_screenshot.body=no s’han pogut recuperar captures de pantalla del {} -games=jocs -game=joc -videoeditor=editor de vídeo -browser=navegador -music=música -development=desenvolupament -network=xarxa -networks=xarxes -graphics=gràfics -system=sistema -utility=utilitat -terminalemulator=terminal -database=base de dades -databases=bases de dades -text editor=editor de text -movies=pel·lícules -movie=pel·lícula -media=mitjans -library=biblioteca -books=llibres -applications=aplicacions -social=social -productivity=productivitat -utilities=utilitats -photo=foto -video=vídeo -entertainment=entreteniment -art=art -design=disseny -reference=referència -personalisation=personalització -education=educació -health=salut -fitness=condició física -science=ciència -news=notícies -weather=el temps -finance=finances -desktopsettings=paràmetres de l’escriptori -texteditor=editor de text -server=servidor -cloud=núvol -instantmessaging=missatgeria -messaging=missatgeria -2dgraphics=gràfics 2d -vectorgraphics=gràfics vectorials -office=oficina -devices=dispositius -security=seguretat -audiovideo=àudio/vídeo -audio=àudio -amusement=diversió -webdevelopment=desenvolupament web -filetransfer=transferència de fitxers -viewer=visor -packagemanager=gestor de paquets -sportsgame=joc d’esport -arcadegame=joc «arcade» -player=jugador -fonts=tipus de lletra -screenshots.bt_next.label=següent -screenshots.bt_back.label=anterior -screenshots,download.running=s’està baixant la imatge -screenshots.download.no_content=No hi ha contingut a mostrar -screenshots.download.no_response=No s’ha trobat la imatge -continue=continua -stable=estable -close=tanca -publisher.verified=verificat -mirror=mirall -emulator=emulador -do_not.install=no instal·leu -repository=dipòsit -details=detalls -communication=comunicació -administration=administració -settings=configuració +action.update.cannot_update_label=Cannot be upgraded +action.update.install_req.fail.body=No s'ha pogut instal·lar {} +action.update.install_req.fail.title=Ha fallat la instal·lació +action.update.label_to_remove=Must be uninstalled +action.update.label_to_upgrade=Will be upgraded +action.update.pkg.required_size=Requires +action.update.required_label=Must be installed +action.update.required_size=Disk space required (download) +action.update.requirements.body=Abans d’actualitzar cal instal·lar les següents {} dependències +action.update.requirements.status=Verificant els requisits +action.update.requirements.title=Requisits d’actualització +action.update.status.checking_sizes=Calculating upgrade size +action.update.success.reboot.line1=Upgrades successfully applied ! +action.update.success.reboot.line2=Some changes may require a system restart to take effect. +action.update.success.reboot.line3=Restart now ? +action.update.summary=Upgrade summary +action.update.total_size=Update size +action.trim_disk.ask=Optimize disc (trim) ? address=adreça -view.components.file_chooser.placeholder=Feu clic aquí per seleccionar -files=fitxers all_files=tots els fitxers -file_chooser.title=Selector de fitxers -message.file.not_exist=Fitxer no existeix -message.file.not_exist.body=El fitxer {} sembla no existir -icon_button.tooltip.disabled=Aquesta acció no està disponible -user=usuari -example.short=e.g -core.config.tab.advanced=Advanced -core.config.tab.general=General -core.config.tab.ui=Interface -core.config.tab.tray=Tray -core.config.tab.types=Types -core.config.locale.label=language -core.config.disk_cache=Disk cache -core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations -core.config.updates.interval=Updates interval -core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) -core.config.downloads=downloads +amount=amount +and=i +any=qualsevol +app.name=nom de l’aplicació +applications=aplicacions +ask=ask +ask.continue=Voleu continuar? +author=autor +back=enrere +bt.app_not_upgrade=No actualitzis +bt.app_upgrade=Actualita +bt.not_now=Not now +cancel=cancel·la +categories=categories +category=categoria +category.2dgraphics=gràfics 2d +category.administration=administració +category.amusement=diversió +category.arcadegame=joc «arcade» +category.art=art +category.audio=àudio +category.audiovideo=àudio/vídeo +category.books=llibres +category.browser=navegador +category.cloud=núvol +category.communication=comunicació +category.database=base de dades +category.databases=bases de dades +category.design=disseny +category.desktopsettings=paràmetres de l’escriptori +category.development=desenvolupament +category.devices=dispositius +category.education=educació +category.emulator=emulador +category.entertainment=entreteniment +category.filetransfer=transferència de fitxers +category.finance=finances +category.fitness=condició física +category.fonts=tipus de lletra +category.game=joc +category.games=jocs +category.graphics=gràfics +category.health=salut +category.instantmessaging=missatgeria +category.library=biblioteca +category.media=mitjans +category.messaging=missatgeria +category.movie=pel·lícula +category.movies=pel·lícules +category.music=música +category.network=xarxa +category.networks=xarxes +category.news=notícies +category.none=None +category.office=oficina +category.packagemanager=gestor de paquets +category.personalisation=personalització +category.photo=foto +category.player=jugador +category.productivity=productivitat +category.reference=referència +category.science=ciència +category.security=seguretat +category.server=servidor +category.settings=configuració +category.social=social +category.sportsgame=joc d’esport +category.system=sistema +category.terminalemulator=terminal +category.text editor=editor de text +category.texteditor=editor de text +category.utilities=utilitats +category.utility=utilitat +category.vectorgraphics=gràfics vectorials +category.video=vídeo +category.videoeditor=editor de vídeo +category.viewer=visor +category.weather=el temps +category.web=web +category.webdevelopment=desenvolupament web +change=modifica +clean=netejar +close=tanca +confirmation=confirmació +console.install_logs.path=Trobareu registres d’instal·lació a {} +continue=continua +copy=copia +core.config.backup=Enabled +core.config.backup.downgrade=Before downgrading +core.config.backup.install=Before installing +core.config.backup.mode=Mode +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy. +core.config.backup.mode.only_one=Single +core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased. +core.config.backup.uninstall=Before uninstalling +core.config.backup.upgrade=Before upgrading core.config.download.icons=Download icons core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table core.config.download.multithreaded=Multithreaded download core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.downloads=downloads +core.config.locale.label=language core.config.mem_cache=Memory storage core.config.mem_cache.data_exp=Data expiration core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.mem_cache.icon_exp=Icons expiration +core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) +core.config.store_password=Single password request +core.config.store_password.tip=Requests the root user password only once core.config.suggestions.activated=Suggestions core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once core.config.system.notifications=Notifications core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.tab.advanced=Advanced +core.config.tab.backup=Backup +core.config.tab.general=General +core.config.tab.tray=Tray +core.config.tab.types=Types +core.config.tab.ui=Interface +core.config.trim_after_update=Optimize disc after upgrading +core.config.trim_after_update.tip=It optimizes the disc after a successful upgrade using "trim". Do not enable this option if your disk is not a SSD that allows trim. +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated core.config.ui.max_displayed=Applications displayed core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray core.config.ui.tray.updates_icon=Update icon -core.config.system.dep_checking=Single system checking -core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once -core.config.suggestions.by_type=Suggestions by type -core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type -core.config.types.tip=Check the application types you want to manage -core.config.ui.auto_scale=Auto scale -core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. -core.config.updates.sort_pkgs=Sort updates -core.config.updates.sort_pkgs.tip=Defines the best update order for the selected applications / packages to avoid issues -core.config.updates.dep_check=Mostra els requisits d’actualització -core.config.updates.dep_check.tip=Mostra tots les dependències addicionals que cal instal·lar abans de començar a actualitzar els seleccionats -settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. -settings.changed.success.reboot=Restart now ? -settings.error=It was not possible to properly change all the settings -locale.en=anglès -locale.es=castellà -locale.pt=portuguès +core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +custom_action.proceed_with=Proceed with {} ? +description=descripció +details=detalls +devices=devices +do_not.install=no instal·leu +downgraded=revertida +download=download +downloading=S’està baixant +error=error +example.short=e.g +exit=surt +file=file +file_chooser.title=Selector de fitxers +files=fitxers +finishing=finishing +gem_selector.question=Quins tipus d’aplicacions voleu que es mostrin aquí? +gem_selector.title=Tipus d’aplicacions +general=general +icon_button.tooltip.disabled=Aquesta acció no està disponible +imported=imported +initialization=inicialització +install=instal·la +installation=instal·lació +installed=instal·lada +internet.required=Us cal una connexió a Internet +interval=interval +latest_version=versió més recent +license=license locale.ca=català locale.de=alemany +locale.en=anglès +locale.es=castellà locale.it=italià +locale.pt=portuguès locale.ru=rus -interval=interval -installation=instal·lació -download=download -clean=netejar -action.update.status.sorting=Determinant el millor ordre d’actualització -action.update.requirements.title=Requisits d’actualització -action.update.requirements.body=Abans d’actualitzar cal instal·lar les següents {} dependències -action.update.install_req.fail.title=Ha fallat la instal·lació -action.update.install_req.fail.body=No s'ha pogut instal·lar {} -action.update.requirements.status=Verificant els requisits -action.update.order.title=Ordre d'actualització -action.update.order.body=L'actualització es realitzarà en l'ordre següent \ No newline at end of file +manage_window.apps_table.row.actions.downgrade=Reverteix +manage_window.apps_table.row.actions.downgrade.popup.body=Voleu revertir «{}»? +manage_window.apps_table.row.actions.history=Historial +manage_window.apps_table.row.actions.info=Informació +manage_window.apps_table.row.actions.install=Instal·la +manage_window.apps_table.row.actions.install.popup.body=Voleu instal·lar «{}» a l’ordinador? +manage_window.apps_table.row.actions.install.popup.title=Instal·lació +manage_window.apps_table.row.actions.refresh=Refresca +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.body=Voleu desinstal·lar «{}» de l’ordinador? +manage_window.apps_table.row.actions.uninstall.popup.title=Desinstal·la +manage_window.apps_table.upgrade_toggle.disabled.tooltip=But it is not possible to upgrade it. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Feu clic aquí per a marcar o desmarcar l’actualització. +manage_window.apps_table.upgrade_toggle.tooltip=Hi ha una actualització d’aquesta aplicació. +manage_window.bt.installed.text=instal·lada +manage_window.bt.installed.tooltip=Feu clic aquí per a mostrar les aplicacions instal·lades +manage_window.bt.refresh.text=Refresca +manage_window.bt.refresh.tooltip=Carrega novament les dades quant a les aplicacions instal·lades +manage_window.bt.suggestions.text=Suggeriments +manage_window.bt.suggestions.tooltip=Feu clic aquí per a mostrar alguns suggeriments d’aplicacions +manage_window.bt.upgrade.text=Actualitza +manage_window.bt.upgrade.tooltip=Actualitza totes les aplicacions marcades +manage_window.bt_custom_actions.tip=Click here to show more actions +manage_window.bt_settings.tooltip=Feu clic aquí per mostrar la configuració +manage_window.checkbox.only_apps=Aplicacions +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.label.updates=Actualitzacions +manage_window.name_filter.placeholder=Filtra per nom +manage_window.name_filter.tooltip=Escriviu aquí per a filtrar aplicacions per nom +manage_window.settings.about=Quant a +manage_window.status.downgrading=S’està revertint +manage_window.status.filtering=S’està filtrant +manage_window.status.history=S’està recuperant l’historial +manage_window.status.info=S’està recuperant la informació +manage_window.status.installed=Loading installed +manage_window.status.installing=S’està instal·lant +manage_window.status.refreshing=S’està refrescant +manage_window.status.refreshing_app=S’està refrescant +manage_window.status.running_app=S’està iniciant «{}» +manage_window.status.screenshots=S’estan recuperant {} captures de pantalla +manage_window.status.searching=S’està cercant +manage_window.status.suggestions=S’estan buscant suggeriments +manage_window.status.uninstalling=S’està desinstal·lant +manage_window.status.upgrading=S’està actualitzant +manage_window.title=les meves aplicacions +manage_window.upgrade_all.popup.body=Voleu actualitzar totes les aplicacions seleccionades? +manage_window.upgrade_all.popup.title=Actualitza +message.file.not_exist=Fitxer no existeix +message.file.not_exist.body=El fitxer {} sembla no existir +mirror=mirall +name=nom +no=no +not_installed=no instal·lada +notification.downgrade.failed=Ha fallat la reversió +notification.install.failed=ha fallat la instal·lació +notification.uninstall.failed=ha fallat la desinstal·lació +notification.update_selected.failed=Ha fallat l’actualització +notification.update_selected.success=aplicacions actualitzades correctament +ok=ok +others=altres +popup.button.cancel=Cancel·la +popup.button.no=No +popup.button.yes=Sí +popup.history.selected.tooltip=Versió actual +popup.history.title=Historial +popup.root.bad_password.body=La contrasenya és incorrecta +popup.root.bad_password.last_try=S’han acabat els intents. S’ha cancel·lat l’operació. +popup.root.bad_password.title=Error +popup.root.continue=continuar +popup.root.title=Requireix la vostra contrasenya per a continuar +popup.screenshots.no_screenshot.body=no s’han pogut recuperar captures de pantalla del {} +prepare_panel.bt_skip.label=saltar +prepare_panel.title.finish=Llest +prepare_panel.title.start=Iniciando +proceed=continua +publisher=proveïdor +publisher.verified=verificat +repository=dipòsit +screenshots.bt_back.label=anterior +screenshots.bt_next.label=següent +screenshots.download.no_content=No hi ha contingut a mostrar +screenshots.download.no_response=No s’ha trobat la imatge +screenshots.download.running=s’està baixant la imatge +settings=configuració +settings.changed.success.reboot=Restart now ? +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.error=It was not possible to properly change all the settings +show=mostra +size=mida +source=font +stable=estable +status.caching_data=S’estan emmagatzemant {} dades a la memòria cau al disc +style=estil +success=success +summary=resum +task.download_categories=Download de categories [ {} ] +type=tipus +uninstall=desinstal·la +uninstalled=desinstal·lada +unknown=desconegut +updates=actualitzacions +user=usuari +version=versió +version.installed=versió instal·lada +version.installed_outdated=la versió instal·lada no està actualitzada +version.latest=versió més recent +version.outdated=no actualitzada +version.unknown=no informat +version.updated=actualitzada +view.components.file_chooser.placeholder=Feu clic aquí per seleccionar +warning=avís +warning.update_available=There is a new {} update available ({}). Checkout the news at {}. +welcome=us donem la benvinguda +window_manage.input_search.placeholder=Cerca +window_manage.input_search.tooltip=Escriviu i premeu Intro per a cercar aplicacions +yes=sí \ No newline at end of file diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index d1002109..2192a5fa 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -1,236 +1,377 @@ -manage_window.title=Meine Anwendungen -manage_window.columns.latest_version=Neueste Version -manage_window.columns.update=Upgraden? -manage_window.apps_table.row.actions.info=Information -manage_window.apps_table.row.actions.history=Verlauf -manage_window.apps_table.row.actions.uninstall.popup.title=Deinstallieren -manage_window.apps_table.row.actions.uninstall.popup.body={} deinstallieren? -manage_window.apps_table.row.actions.install.popup.title=Installation -manage_window.apps_table.row.actions.install.popup.body={} installieren? -manage_window.apps_table.row.actions.downgrade=Downgraden -manage_window.apps_table.row.actions.downgrade.popup.body={} wirklich downgraden? -manage_window.apps_table.row.actions.install=Installieren -manage_window.apps_table.row.actions.refresh=Aktualisieren -manage_window.apps_table.upgrade_toggle.tooltip=Für diese Anwendung ist ein Update verfügbar. Klicken Sie hier, um das Update zur Auswahl hinzuzufügen oder es zu entfernen. -manage_window.checkbox.only_apps=Apps -window_manage.input_search.placeholder=Suchbegriff -window_manage.input_search.tooltip=Suchbegriff eingeben und Enter drücken, um zu suchen -manage_window.label.updates=Updates -manage_window.status.refreshing=Aktualisieren -manage_window.status.upgrading=Upgraden -manage_window.status.uninstalling=Deinstallieren -manage_window.status.downgrading=Downgraden -manage_window.status.info=Informationen laden -manage_window.status.history=Verlauf laden -manage_window.status.searching=Suchen -manage_window.status.installing=Installieren -manage_window.status.refreshing_app=Aktualisieren -manage_window.status.running_app={} wird gestartet -manage_window.status.filtering=Filtern -manage_window.status.screenshots={} Screenshots laden -manage_window.status.suggestions=Retrieving suggestions -manage_window.bt.refresh.text=Aktualisieren -manage_window.bt.refresh.tooltip=Informationen über installierte Anwendungen aktualisieren -manage_window.bt.upgrade.text=Upgraden -manage_window.bt.upgrade.tooltip=Alle ausgewählten Anwendungen upgraden -manage_window.bt.installed.text=installiert -manage_window.bt.installed.tooltip=Installierte Anwendungen anzeigen -manage_window.bt.suggestions.text=Vorschläge -manage_window.bt.suggestions.tooltip=Vorschläge für Apps anzeigen -manage_window.checkbox.show_details=Details anzeigen -popup.root.title=Zum Fortfahren wird das Passwort benötigt. -popup.root.continue=Fortfahren -popup.root.bad_password.title=Fehler -popup.root.bad_password.body=Ungültiges Passwort -popup.root.bad_password.last_try=Zu viele Fehlversuche. Aktion abgebrochen. -popup.history.title=Verlauf -popup.history.selected.tooltip=Aktuelle Version -popup.button.yes=Ja -popup.button.no=Nein -popup.button.cancel=Abbrechen -tray.action.manage=Anwendungen -tray.action.exit=Beenden -tray.action.about=Über -tray.action.refreshing=Aktualisieren +Australia=Australia +Austria=Austria +Bangladesh=Bangladesh +Belarus=Belarus +Belgium=Belgium +Brazil=Brazil +Bulgaria=Bulgaria +Canada=Canada +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=Czech +Denmark=Denmark +Ecuador=Ecuador +France=France +Georgia=Georgia +Germany=Germany +Greece=Greece +Hong Kong=Hong Kong +Hungary=Hungary +Iceland=Iceland +India=India +Indonesia=Indonesia +Iran=Iran +Italy=Italy +Japan=Japan +Kenya=Kenya +Netherlands=Netherlands +New Zealand=New Zealand +Norway=Norway +Philippines=Philippines +Poland=Poland +Portugal=Portugal +Russia=Russia +Singapore=Singapore +South Africa=South Africa +South Korea=South Korea +Spain=Spain +Sweden=Sweden +Switzerland=Switzerland +Taiwan=Taiwan +Thailand=Thailand +Turkey=Turkey +Ukraine=Ukraine +United Kingdom=United Kingdom +United States=United States +action.backup.error.create=It was not possible to generate a new system copy +action.backup.error.delete=It was not possible to delete the old system copies +action.backup.error.proceed=Proceed anyway ? +action.backup.invalid_mode=Invalid backup mode +action.backup.msg=Do you want to generate a system copy before proceeding ? +action.backup.substatus.create=Generating a new system backup +action.backup.substatus.delete=Removing old system backups +action.cancelled=Aktion vom Nutzer abgebrochen +action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim.error=There was a problem while optimizing the disk +action.failed=Aktion fehlgeschlagen +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history action.info.tooltip=Informationen über diese Anwendung anzeigen +action.not_allowed=Action not allowed +action.request_reboot.title=System restart +action.run.tooltip=Die Anwendung ausführen action.screenshots.tooltip=Fotos der Anwendung anzeigen action.settings.tooltip=Weitere Optionen anzeigen -notification.update={} Update verfügbar -notification.updates={} Updates verfügbar -notification.update_selected.success=Anwendung(en) erfolgreich aktualisiert -notification.update_selected.failed=Aktualisierung fehlgeschlagen -notification.install.failed=Installation fehlgeschlagen -notification.uninstall.failed=Deinstallation fehlgeschlagen -notification.downgrade.failed=Downgrade fehlgeschlagen -about.info.desc=Grafisches Interface, um Linux-Anwendungen zu verwalten -about.info.link=Mehr Informationen unter -about.info.license=Freie Lizenz -about.info.rate.question=Gefällt Ihnen diese Anwendung? -about.info.rate.answer=Unterstützen Sie uns auf GitHub mit einem Stern. -about.info.trouble.question=Probleme? -about.info.trouble.answer=Eröffnen Sie ein GitHub-Issue, damit die Entwickler Ihnen helfen können. -yes=Ja -no=Nein -version.updated=geupdated -version.outdated=veraltet -name=Name -version=Version -latest_version=Aktuellste Version -description=Beschreibung -type=Typ -installed=installiert -uninstalled=deinstalliert -not_installed=nicht installiert -downgraded=gedowngraded -others=Weitere -internet.required=Internet Verbindung benötigt -updates=Updates -version.installed=Installierte Version -version.latest=Aktuellste Version -version.installed_outdated=Die installierte Version ist veraltet -version.unknown=unbekannt -app.name=Anwendungsname -warning=Warnung -install=Installation -uninstall=Deinstallation -bt.app_upgrade=Upgrade -bt.app_not_upgrade=Kein Upgrade -manage_window.upgrade_all.popup.title=Upgrade -manage_window.upgrade_all.popup.body=Alle ausgewählten Anwendungen upgraden -manage_window.settings.about=Über -confirmation=Bestätigung -and=und -error=Fehler -action.cancelled=Aktion vom Nutzer abgebrochen -copy=Kopieren -back=Zurück -show=Anzeigen -ask.continue=Fortfahren? -cancel=Abbrechen -status.caching_data={} Daten auf der Festplatte zwischenspeichern -action.run.tooltip=Die Anwendung ausführen -any=Alle -manage_window.name_filter.tooltip=Apps nach Name filtern -manage_window.name_filter.placeholder=Nach Name filtern -publisher=Herausgeber -unknown=unbekannt -welcome=Willkommen -gem_selector.title=Anwendungstypen -gem_selector.question=Welche Typen von Anwendungen sollen verwaltet werden? -proceed=Fortfahren -change=Anwenden -exit=Beenden -style=Stil -manage_window.bt_settings.tooltip=Einstellungen anpassen -downloading=Download -console.install_logs.path=Die Installationslogs befinden sich unter {} -author=Autor -source=Herkunft -size=Größe -category=Kategorie -categories=Kategorien -summary=Zusammenfassung -popup.screenshots.no_screenshot.body={} Screenshots konnten nicht heruntergeladen werden. -terminalemulator=Terminal -desktopsettings=Desktop-Einstellungen -texteditor=Texteditor -screenshots.bt_next.label=Fortfahren -screenshots.bt_back.label=Zurück -instantmessaging=Kommunikation -messaging=messaging -2dgraphics=2D-Grafik -vectorgraphics=Vektorgrafik -audiovideo=Audio/Video -audio=Audio -webdevelopment=Web-Entwicklung -filetransfer=Dateiübertragung -packagemanager=Paketmanager -sportsgame=Sportspiel -arcadegame=Arcade-Spiel -player=Spieler -screenshots,download.running=Screenshot herunterladen -screenshots.download.no_content=Kein Inhalt -screenshots.download.no_response=Screenshot nicht gefunden -continue=Fortfahren -stable=Stabil -close=Schließen -publisher.verified=Geprüft -emulator=Emulator -do_not.install=nicht installieren -repository=Repository -details=Details -communication=Kommunikation -administration=Verwaltung -settings=Einstellungen +action.update.cannot_update_label=Cannot be upgraded +action.update.install_req.fail.body={} konnte nicht erfolgreich installiert werden +action.update.install_req.fail.title=Installation fehlgeschlagen +action.update.label_to_remove=Must be uninstalled +action.update.label_to_upgrade=Will be upgraded +action.update.pkg.required_size=Requires +action.update.required_label=Must be installed +action.update.required_size=Disk space required (download) +action.update.requirements.body=Die folgenden {} Abhängigkeiten müssen zusätzlich installiert werden +action.update.requirements.status=Prüfe Voraussetzungen +action.update.requirements.title=Upgrade-Voraussetzungen +action.update.status.checking_sizes=Calculating upgrade size +action.update.success.reboot.line1=Upgrades successfully applied ! +action.update.success.reboot.line2=Some changes may require a system restart to take effect. +action.update.success.reboot.line3=Restart now ? +action.update.summary=Upgrade summary +action.update.total_size=Update size +action.trim_disk.ask=Optimize disc (trim) ? address=Adresse -view.components.file_chooser.placeholder=Auswählen -files=Dateien all_files=Alle Dateien -file_chooser.title=Dateiauswahl -message.file.not_exist=Datei existiert nicht -message.file.not_exist.body=Die Datei {} scheint nicht zu existieren -development=Entwicklung -icon_button.tooltip.disabled=Diese Aktion ist nicht verfügbar -user=Benutzer -example.short=z.B. -core.config.tab.advanced=Erweitert -core.config.tab.general=Allgemein -core.config.tab.ui=Oberfläche -core.config.tab.tray=Statusleiste -core.config.tab.types=Typen -core.config.locale.label=Sprache -core.config.disk_cache=Festplatten-Cache -core.config.disk_cache.tip=Informationen über die instsallierten Anwendungen werden auf der Festplatte zwischengespeichert, um die Anwendung zu beschleunigen. -core.config.updates.interval=Update-Intervall -core.config.updates.interval.tip=Das Intervall (in Sekunden), in dem nach Updates für die installierten Anwendungen gesucht wird. -core.config.downloads=Downloads +amount=amount +and=und +any=Alle +app.name=Anwendungsname +applications=applications +ask=ask +ask.continue=Fortfahren? +author=Autor +back=Zurück +bt.app_not_upgrade=Kein Upgrade +bt.app_upgrade=Upgrade +bt.not_now=Not now +cancel=Abbrechen +categories=Kategorien +category=Kategorie +category.2dgraphics=2D-Grafik +category.administration=Verwaltung +category.amusement=amusement +category.arcadegame=Arcade-Spiel +category.art=art +category.audio=Audio +category.audiovideo=Audio/Video +category.books=books +category.browser=browser +category.cloud=cloud +category.communication=Kommunikation +category.database=database +category.databases=databases +category.design=design +category.desktopsettings=Desktop-Einstellungen +category.development=Entwicklung +category.education=education +category.emulator=Emulator +category.entertainment=entertainment +category.filetransfer=Dateiübertragung +category.finance=finance +category.fitness=fitness +category.fonts=fonts +category.game=game +category.games=games +category.graphics=graphics +category.health=health +category.instantmessaging=Kommunikation +category.library=library +category.media=media +category.messaging=messaging +category.movie=movie +category.movies=movies +category.music=music +category.network=network +category.networks=networks +category.news=news +category.none=None +category.office=office +category.packagemanager=Paketmanager +category.personalisation=personalization +category.photo=photo +category.player=Spieler +category.productivity=productivity +category.reference=reference +category.science=science +category.security=security +category.server=server +category.settings=Einstellungen +category.social=social +category.sportsgame=Sportspiel +category.system=system +category.terminalemulator=Terminal +category.text editor=text editor +category.texteditor=Texteditor +category.utilities=utilities +category.utility=utility +category.vectorgraphics=Vektorgrafik +category.video=video +category.videoeditor=video editor +category.viewer=viewer +category.weather=weather +category.web=web +category.webdevelopment=Web-Entwicklung +change=Anwenden +clean=Bereinigen +close=Schließen +confirmation=Bestätigung +console.install_logs.path=Die Installationslogs befinden sich unter {} +continue=Fortfahren +copy=Kopieren +core.config.backup=Enabled +core.config.backup.downgrade=Before downgrading +core.config.backup.install=Before installing +core.config.backup.mode=Mode +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy. +core.config.backup.mode.only_one=Single +core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased. +core.config.backup.uninstall=Before uninstalling +core.config.backup.upgrade=Before upgrading core.config.download.icons=Download Icons core.config.download.icons.tip=Falls aktiviert werden die Anwendungs-Icons in der Tabelle angezeigt core.config.download.multithreaded=Paralleler Download core.config.download.multithreaded.tip=Falls aktiviert werden Downloads parallel (mit mehreren Threads) heruntergeladen. Dazu muss aria installiert sein. +core.config.downloads=Downloads +core.config.locale.label=Sprache core.config.mem_cache=RAM Cache core.config.mem_cache.data_exp=Ablauf des Caches core.config.mem_cache.data_exp.tip=Bestimmt die Dauer des In-Memory-Caches +core.config.mem_cache.icon_exp=Icons expiration +core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) +core.config.store_password=Single password request +core.config.store_password.tip=Requests the root user password only once core.config.suggestions.activated=Vorschläge core.config.suggestions.activated.tip=Falls aktiviert werden Anwendungsvorschläge angezeigt +core.config.suggestions.by_type=Vorschläge pro Typ +core.config.suggestions.by_type.tip=Maximale Anzahl an Vorschlägen pro Anwendungstyp +core.config.system.dep_checking=Einmaliger System-Check +core.config.system.dep_checking.tip=Falls aktiviert wird die Verfügbakeitsprüfung für Systemkomponenten nur einmal ausgeführt core.config.system.notifications=Benachrichtigungen core.config.system.notifications.tip=Falls aktiviert werden Benachrichtigungen für Updates und beendete Vorgänge angezeigt +core.config.tab.advanced=Erweitert +core.config.tab.backup=Backup +core.config.tab.general=Allgemein +core.config.tab.tray=Statusleiste +core.config.tab.types=Typen +core.config.tab.ui=Oberfläche +core.config.trim_after_update=Optimize disc after upgrading +core.config.trim_after_update.tip=It optimizes the disc after a successful upgrade using "trim". Do not enable this option if your disk is not a SSD that allows trim. +core.config.types.tip=Wählen Sie die Anwendungstypen, die Sie verwalten wollen +core.config.ui.auto_scale=Automatische Skalierung +core.config.ui.auto_scale.tip=Aktiviert den automatischen Skalierungsfaktor ({}), welcher bei einigen Umgebungen Skalierungsprobleme behebt core.config.ui.hdpi=HiDPI core.config.ui.hdpi.tip=Falls aktiviert werden Optimierungen für hochauflösende Displays vorgenommen core.config.ui.max_displayed=Anzahl der Anwendungen core.config.ui.max_displayed.tip=Maximale Anzahl von Anwendungen, die in der Tabelle angezeigt werden core.config.ui.tray.default_icon=Anwendungs-Icon +core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray core.config.ui.tray.updates_icon=Icon bei Updates -core.config.system.dep_checking=Einmaliger System-Check -core.config.system.dep_checking.tip=Falls aktiviert wird die Verfügbakeitsprüfung für Systemkomponenten nur einmal ausgeführt -core.config.suggestions.by_type=Vorschläge pro Typ -core.config.suggestions.by_type.tip=Maximale Anzahl an Vorschlägen pro Anwendungstyp -core.config.types.tip=Wählen Sie die Anwendungstypen, die Sie verwalten wollen -core.config.ui.auto_scale=Automatische Skalierung -core.config.ui.auto_scale.tip=Aktiviert den automatischen Skalierungsfaktor ({}), welcher bei einigen Umgebungen Skalierungsprobleme behebt -core.config.updates.sort_pkgs=Updates sortieren -core.config.updates.sort_pkgs.tip=Sortierreihenfolge für ausgewählte Anwendungen/Pakete zur Vermeidung von Problemen -core.config.updates.dep_check=Update-Voraussetzungen anzeigen -core.config.updates.dep_check.tip=Zeigt alle Voraussetzungen an, damit das Update der ausgewählten Anwendungen erfolgen kann -settings.changed.success.warning=Einstellungen gespeichert. Einige werden erst beim nächsten Anwedungsstart sichtbar. -settings.changed.success.reboot=Anwendung neustarten? -settings.error=Es konnten nicht alle Einstellungen übernommen werden. -locale.en=Englisch -locale.es=Spanisch -locale.pt=Portugiesisch +core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available +core.config.updates.interval=Update-Intervall +core.config.updates.interval.tip=Das Intervall (in Sekunden), in dem nach Updates für die installierten Anwendungen gesucht wird. +custom_action.proceed_with=Proceed with {} ? +description=Beschreibung +details=Details +devices=devices +do_not.install=nicht installieren +downgraded=gedowngraded +download=Download +downloading=Download +error=Fehler +example.short=z.B. +exit=Beenden +file=file +file_chooser.title=Dateiauswahl +files=Dateien +finishing=finishing +gem_selector.question=Welche Typen von Anwendungen sollen verwaltet werden? +gem_selector.title=Anwendungstypen +general=Allgemein +icon_button.tooltip.disabled=Diese Aktion ist nicht verfügbar +imported=imported +initialization=Initialisierung +install=Installation +installation=Installation +installed=installiert +internet.required=Internet Verbindung benötigt +interval=Intervall +latest_version=Aktuellste Version +license=license locale.ca=Katalanisch locale.de=Deutsch +locale.en=Englisch +locale.es=Spanisch locale.it=Italienisch +locale.pt=Portugiesisch locale.ru=Russisch -interval=Intervall -installation=Installation -download=Download -clean=Bereinigen -action.update.status.sorting=Sortierreihenfolge -action.update.requirements.title=Upgrade-Voraussetzungen -action.update.requirements.body=Die folgenden {} Abhängigkeiten müssen zusätzlich installiert werden -action.update.install_req.fail.title=Installation fehlgeschlagen -action.update.install_req.fail.body={} konnte nicht erfolgreich installiert werden -action.update.requirements.status=Prüfe Voraussetzungen -action.update.order.title=Upgrade-Reihenfolge -action.update.order.body=Das Upgrade in der folgenden Reihenfolge durchgeführt \ No newline at end of file +manage_window.apps_table.row.actions.downgrade=Downgraden +manage_window.apps_table.row.actions.downgrade.popup.body={} wirklich downgraden? +manage_window.apps_table.row.actions.history=Verlauf +manage_window.apps_table.row.actions.info=Information +manage_window.apps_table.row.actions.install=Installieren +manage_window.apps_table.row.actions.install.popup.body={} installieren? +manage_window.apps_table.row.actions.install.popup.title=Installation +manage_window.apps_table.row.actions.refresh=Aktualisieren +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.body={} deinstallieren? +manage_window.apps_table.row.actions.uninstall.popup.title=Deinstallieren +manage_window.apps_table.upgrade_toggle.disabled.tooltip=But it is not possible to upgrade it. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Klicken Sie hier, um das Update zur Auswahl hinzuzufügen oder es zu entfernen. +manage_window.apps_table.upgrade_toggle.tooltip=Für diese Anwendung ist ein Update verfügbar. +manage_window.bt.installed.text=installiert +manage_window.bt.installed.tooltip=Installierte Anwendungen anzeigen +manage_window.bt.refresh.text=Aktualisieren +manage_window.bt.refresh.tooltip=Informationen über installierte Anwendungen aktualisieren +manage_window.bt.suggestions.text=Vorschläge +manage_window.bt.suggestions.tooltip=Vorschläge für Apps anzeigen +manage_window.bt.upgrade.text=Upgraden +manage_window.bt.upgrade.tooltip=Alle ausgewählten Anwendungen upgraden +manage_window.bt_custom_actions.tip=Click here to show more actions +manage_window.bt_settings.tooltip=Einstellungen anpassen +manage_window.checkbox.only_apps=Apps +manage_window.checkbox.show_details=Details anzeigen +manage_window.columns.installed=Installed +manage_window.columns.latest_version=Neueste Version +manage_window.columns.update=Upgraden? +manage_window.label.updates=Updates +manage_window.name_filter.placeholder=Nach Name filtern +manage_window.name_filter.tooltip=Apps nach Name filtern +manage_window.settings.about=Über +manage_window.status.downgrading=Downgraden +manage_window.status.filtering=Filtern +manage_window.status.history=Verlauf laden +manage_window.status.info=Informationen laden +manage_window.status.installed=Loading installed +manage_window.status.installing=Installieren +manage_window.status.refreshing=Aktualisieren +manage_window.status.refreshing_app=Aktualisieren +manage_window.status.running_app={} wird gestartet +manage_window.status.screenshots={} Screenshots laden +manage_window.status.searching=Suchen +manage_window.status.suggestions=Retrieving suggestions +manage_window.status.uninstalling=Deinstallieren +manage_window.status.upgrading=Upgraden +manage_window.title=Meine Anwendungen +manage_window.upgrade_all.popup.body=Alle ausgewählten Anwendungen upgraden +manage_window.upgrade_all.popup.title=Upgrade +message.file.not_exist=Datei existiert nicht +message.file.not_exist.body=Die Datei {} scheint nicht zu existieren +mirror=mirror +name=Name +no=Nein +not_installed=nicht installiert +notification.downgrade.failed=Downgrade fehlgeschlagen +notification.install.failed=Installation fehlgeschlagen +notification.uninstall.failed=Deinstallation fehlgeschlagen +notification.update_selected.failed=Aktualisierung fehlgeschlagen +notification.update_selected.success=Anwendung(en) erfolgreich aktualisiert +ok=ok +others=Weitere +popup.button.cancel=Abbrechen +popup.button.no=Nein +popup.button.yes=Ja +popup.history.selected.tooltip=Aktuelle Version +popup.history.title=Verlauf +popup.root.bad_password.body=Ungültiges Passwort +popup.root.bad_password.last_try=Zu viele Fehlversuche. Aktion abgebrochen. +popup.root.bad_password.title=Fehler +popup.root.continue=Fortfahren +popup.root.title=Zum Fortfahren wird das Passwort benötigt. +popup.screenshots.no_screenshot.body={} Screenshots konnten nicht heruntergeladen werden. +prepare_panel.bt_skip.label=skip +prepare_panel.title.finish=Bereit +prepare_panel.title.start=Initialisierung +proceed=Fortfahren +publisher=Herausgeber +publisher.verified=Geprüft +repository=Repository +screenshots.bt_back.label=Zurück +screenshots.bt_next.label=Fortfahren +screenshots.download.no_content=Kein Inhalt +screenshots.download.no_response=Screenshot nicht gefunden +screenshots.download.running=Screenshot herunterladen +settings=Einstellungen +settings.changed.success.reboot=Anwendung neustarten? +settings.changed.success.warning=Einstellungen gespeichert. Einige werden erst beim nächsten Anwedungsstart sichtbar. +settings.error=Es konnten nicht alle Einstellungen übernommen werden. +show=Anzeigen +size=Größe +source=Herkunft +stable=Stabil +status.caching_data={} Daten auf der Festplatte zwischenspeichern +style=Stil +success=success +summary=Zusammenfassung +task.download_categories=Downloading categories [ {} ] +type=Typ +uninstall=Deinstallation +uninstalled=deinstalliert +unknown=unbekannt +updates=Updates +user=Benutzer +version=Version +version.installed=Installierte Version +version.installed_outdated=Die installierte Version ist veraltet +version.latest=Aktuellste Version +version.outdated=veraltet +version.unknown=unbekannt +version.updated=geupdated +view.components.file_chooser.placeholder=Auswählen +warning=Warnung +warning.update_available=There is a new {} update available ({}). Checkout the news at {}. +welcome=Willkommen +window_manage.input_search.placeholder=Suchbegriff +window_manage.input_search.tooltip=Suchbegriff eingeben und Enter drücken, um zu suchen +yes=Ja \ No newline at end of file diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 57f05a51..bf9dc100 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -1,243 +1,377 @@ -manage_window.title=my applications -manage_window.columns.latest_version=Latest Version -manage_window.columns.update=Upgrade ? -manage_window.apps_table.row.actions.info=Information -manage_window.apps_table.row.actions.history=History -manage_window.apps_table.row.actions.uninstall.popup.title=Uninstall -manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} from your device ? -manage_window.apps_table.row.actions.install.popup.title=Installation -manage_window.apps_table.row.actions.install.popup.body=Install {} on your device ? -manage_window.apps_table.row.actions.downgrade=Downgrade -manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ? -manage_window.apps_table.row.actions.install=Install -manage_window.apps_table.row.actions.refresh=Refresh -manage_window.apps_table.upgrade_toggle.tooltip=There is an update for this application. Click here to check or uncheck the update -manage_window.checkbox.only_apps=Apps -window_manage.input_search.placeholder=Search -window_manage.input_search.tooltip=Type and press ENTER to search for applications -manage_window.label.updates=Updates -manage_window.status.refreshing=Refreshing -manage_window.status.upgrading=Upgrading -manage_window.status.uninstalling=Uninstalling -manage_window.status.downgrading=Downgrading -manage_window.status.info=Retrieving information -manage_window.status.history=Retrieving history -manage_window.status.searching=Searching -manage_window.status.installing=Installing -manage_window.status.refreshing_app=Refreshing -manage_window.status.running_app=Launching {} -manage_window.status.filtering=Filtering -manage_window.status.screenshots=Retrieving {} screenshots -manage_window.status.suggestions=Looking for suggestions -manage_window.bt.refresh.text=Refresh -manage_window.bt.refresh.tooltip=Reload the data about installed applications -manage_window.bt.upgrade.text=Upgrade -manage_window.bt.upgrade.tooltip=Upgrade all checked applications -manage_window.bt.installed.text=installed -manage_window.bt.installed.tooltip=Click here to show the installed applications -manage_window.bt.suggestions.text=Suggestions -manage_window.bt.suggestions.tooltip=Click here to show some applications suggestions -manage_window.checkbox.show_details=Show details -popup.root.title=Requires your password to continue -popup.root.continue=continue -popup.root.bad_password.title=Error -popup.root.bad_password.body=Wrong password -popup.root.bad_password.last_try=Attempts finished. Action cancelled. -popup.history.title=History -popup.history.selected.tooltip=Current version -popup.button.yes=Yes -popup.button.no=No -popup.button.cancel=Cancel -tray.action.manage=Applications -tray.action.exit=Quit -tray.action.about=About -tray.action.refreshing=Refreshing +Australia=Australia +Austria=Austria +Bangladesh=Bangladesh +Belarus=Belarus +Belgium=Belgium +Brazil=Brazil +Bulgaria=Bulgaria +Canada=Canada +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=Czech +Denmark=Denmark +Ecuador=Ecuador +France=France +Georgia=Georgia +Germany=Germany +Greece=Greece +Hong Kong=Hong Kong +Hungary=Hungary +Iceland=Iceland +India=India +Indonesia=Indonesia +Iran=Iran +Italy=Italy +Japan=Japan +Kenya=Kenya +Netherlands=Netherlands +New Zealand=New Zealand +Norway=Norway +Philippines=Philippines +Poland=Poland +Portugal=Portugal +Russia=Russia +Singapore=Singapore +South Africa=South Africa +South Korea=South Korea +Spain=Spain +Sweden=Sweden +Switzerland=Switzerland +Taiwan=Taiwan +Thailand=Thailand +Turkey=Turkey +Ukraine=Ukraine +United Kingdom=United Kingdom +United States=United States +action.backup.error.create=It was not possible to generate a new system copy +action.backup.error.delete=It was not possible to delete the old system copies +action.backup.error.proceed=Proceed anyway ? +action.backup.invalid_mode=Invalid backup mode +action.backup.msg=Do you want to generate a system copy before proceeding ? +action.backup.substatus.create=Generating a new system backup +action.backup.substatus.delete=Removing old system backups +action.cancelled=operation cancelled by the user +action.disk_trim.error=There was a problem while optimizing the disk +action.disk_trim=Optimizing disc ( TRIM ) +action.failed=Action failed +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history action.info.tooltip=Click here to see information about this application +action.not_allowed=Action not allowed +action.request_reboot.title=System restart +action.run.tooltip=Click here to launch this application action.screenshots.tooltip=Click here to see some pictures of this application action.settings.tooltip=Click here to open extra actions -notification.update={} available update -notification.updates={} available updates -notification.update_selected.success=app(s) updated successfully -notification.update_selected.failed=Update failed -notification.install.failed=installation failed -notification.uninstall.failed=uninstallation failed -notification.downgrade.failed=Failed to downgrade -about.info.desc=Graphical interface for managing your Linux applications -about.info.link=More information at -about.info.license=Free license -about.info.rate.question=Like this tool ? -about.info.rate.answer=Give it a star on GitHub to strengthen the project -about.info.trouble.question=Any trouble ? -about.info.trouble.answer=Open a GitHub issue so the developers can help you -yes=yes -no=no -version.updated=updated -version.outdated=outdated -name=name -version=version -latest_version=latest version -description=description -type=type -installed=installed -uninstalled=uninstalled -not_installed=not installed -downgraded=downgraded -others=others -internet.required=Internet connection is required -updates=updates -version.installed=installed version -version.latest=latest version -version.installed_outdated=the installed version is outdated -version.unknown=not informed -app.name=application name -warning=warning -install=install -uninstall=uninstall -bt.app_upgrade=Upgrade -bt.app_not_upgrade=Don't upgrade -manage_window.upgrade_all.popup.title=Upgrade -manage_window.upgrade_all.popup.body=Upgrade all selected applications ? -manage_window.settings.about=About -confirmation=confirmation -and=and -error=error -action.cancelled=operation cancelled by the user -copy=copy -back=back -show=show -ask.continue=Continue ? -cancel=cancel -status.caching_data=Caching {} data to disk -action.run.tooltip=Click here to launch this application -any=any -manage_window.name_filter.tooltip=Type here to filter apps by name -manage_window.name_filter.placeholder=Filter by name -publisher=publisher -unknown=unknown -welcome=welcome -gem_selector.title=Application types -gem_selector.question=What types of applications do you want to find here ? -proceed=proceed -change=change -exit=exit -style=style -manage_window.bt_settings.tooltip=Click here to display the settings -downloading=Downloading -console.install_logs.path=Installation logs can be found at {} -author=author -source=source -size=size -category=category -categories=categories -summary=summary -popup.screenshots.no_screenshot.body=it was not possible to retrieve {} screenshots -terminalemulator=terminal -desktopsettings=desktop settings -texteditor=text editor -screenshots.bt_next.label=next -screenshots.bt_back.label=previous -instantmessaging=messaging -2dgraphics=2d graphics -vectorgraphics=vector graphics -audiovideo=audio / video -webdevelopment=web development -filetransfer=file transfer -packagemanager=package manager -sportsgame=sports game -arcadegame=arcade game -player=player -screenshots,download.running=downloading image -screenshots.download.no_content=No content to display -screenshots.download.no_response=Image not found -continue=continue -stable=stable -close=close -publisher.verified=verified -mirror=mirror -emulator=emulator -do_not.install=don't install -repository=repository -details=details -communication=communication -web=web -office=office -messaging=messaging -administration=administration -audio=audio -video=video -settings=settings +action.update.cannot_update_label=Cannot be upgraded +action.update.install_req.fail.body=It was not possible to install {} +action.update.install_req.fail.title=Installation failed +action.update.label_to_remove=Must be uninstalled +action.update.label_to_upgrade=Will be upgraded +action.update.pkg.required_size=Requires +action.update.required_label=Must be installed +action.update.required_size=Disk space required (download) +action.update.requirements.body=The following {} dependencies must be installed before upgrading +action.update.requirements.status=Checking requirements +action.update.requirements.title=Upgrade requirements +action.update.status.checking_sizes=Calculating the upgrades size +action.update.success.reboot.line1=Upgrades successfully applied ! +action.update.success.reboot.line2=Some changes may require a system restart to take effect. +action.update.success.reboot.line3=Restart now ? +action.update.summary=Upgrade summary +action.update.total_size=Update size +action.trim_disk.ask=Optimize disc (trim) ? address=address -view.components.file_chooser.placeholder=Click here to select -files=files all_files=all files -file_chooser.title=File selector -message.file.not_exist=File does not exist -message.file.not_exist.body=The file {} seems not to exist -development=development -icon_button.tooltip.disabled=This action is unavailable -user=user -example.short=e.g +amount=amount +and=and +any=any +app.name=application name +applications=applications +ask.continue=Continue ? +ask=ask +author=author +back=back +bt.app_not_upgrade=Don't upgrade +bt.app_upgrade=Upgrade +bt.not_now=Not now +cancel=cancel +categories=categories +category.2dgraphics=2d graphics +category.administration=administration +category.amusement=amusement +category.arcadegame=arcade game +category.art=art +category.audio=audio +category.audiovideo=audio / video +category.books=books +category.browser=browser +category.cloud=cloud +category.communication=communication +category.database=database +category.databases=databases +category.design=design +category.desktopsettings=desktop settings +category.development=development +category.education=education +category.emulator=emulator +category.entertainment=entertainment +category.filetransfer=file transfer +category.finance=finance +category.fitness=fitness +category.fonts=fonts +category.game=game +category.games=games +category.graphics=graphics +category.health=health +category.instantmessaging=messaging +category.library=library +category.media=media +category.messaging=messaging +category.movie=movie +category.movies=movies +category.music=music +category.network=network +category.networks=networks +category.news=news +category.none=None +category.office=office +category.packagemanager=package manager +category.personalisation=personalization +category.photo=photo +category.player=player +category.productivity=productivity +category.reference=reference +category.science=science +category.security=security +category.server=server +category.settings=settings +category.social=social +category.sportsgame=sports game +category.system=system +category.terminalemulator=terminal +category.text editor=text editor +category.texteditor=text editor +category.utilities=utilities +category.utility=utility +category.vectorgraphics=vector graphics +category.video=video +category.videoeditor=video editor +category.viewer=viewer +category.weather=weather +category.web=web +category.webdevelopment=web development +category=category +change=change +clean=clean +close=close +confirmation=confirmation +console.install_logs.path=Installation logs can be found at {} +continue=continue +copy=copy +core.config.backup.downgrade=Before downgrading +core.config.backup.install=Before installing +core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy. +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased. +core.config.backup.mode.only_one=Single +core.config.backup.mode=Mode +core.config.backup.uninstall=Before uninstalling +core.config.backup.upgrade=Before upgrading +core.config.backup=Enabled +core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table +core.config.download.icons=Download icons +core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.download.multithreaded=Multithreaded download +core.config.downloads=downloads +core.config.locale.label=language +core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.mem_cache.data_exp=Data expiration +core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) +core.config.mem_cache.icon_exp=Icons expiration +core.config.mem_cache=Memory storage +core.config.store_password.tip=It requests the root user password only once +core.config.store_password=Single password request +core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.suggestions.activated=Suggestions +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.suggestions.by_type=Suggestions by type +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once +core.config.system.dep_checking=Single system checking +core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.system.notifications=Notifications core.config.tab.advanced=Advanced +core.config.tab.backup=Backup core.config.tab.general=General -core.config.tab.ui=Interface core.config.tab.tray=Tray core.config.tab.types=Types -core.config.locale.label=language -core.config.disk_cache=Disk cache -core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations -core.config.updates.interval=Updates interval -core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) -core.config.download.icons=Download icons -core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table -core.config.download.multithreaded=Multithreaded download -core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed -core.config.mem_cache=Memory storage -core.config.mem_cache.data_exp=Data expiration -core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) -core.config.mem_cache.icon_exp=Icons expiration -core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) -core.config.suggestions.activated=Suggestions -core.config.suggestions.activated.tip=If new applications can be suggested for installation -core.config.system.notifications=Notifications -core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates -core.config.ui.hdpi=HDPI -core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated -core.config.ui.max_displayed=Applications displayed -core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table -core.config.ui.tray.default_icon=Default icon -core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray -core.config.ui.tray.updates_icon=Update icon -core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available -core.config.system.dep_checking=Single system checking -core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once -core.config.suggestions.by_type=Suggestions by type -core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.tab.ui=Interface +core.config.trim_after_update.tip=It optimizes the disc after a successfull upgrade using "trim". Do not enable this option if your disk is not a SSD that allows trim. +core.config.trim_after_update=Optimize disc after upgrading core.config.types.tip=Check the application types you want to manage -core.config.ui.auto_scale=Auto scale core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. -core.config.updates.sort_pkgs=Sort updates -core.config.updates.sort_pkgs.tip=Defines the best update order for the selected applications / packages to avoid issues -core.config.updates.dep_check=Show update requirements -core.config.updates.dep_check.tip=Displays all dependencies that need to be installed before starting to update selected ones -settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. -settings.changed.success.reboot=Restart now ? -settings.error=It was not possible to properly change all the settings -locale.en=english -locale.es=spanish -locale.pt=portuguese +core.config.ui.auto_scale=Auto scale +core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated +core.config.ui.hdpi=HDPI +core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table +core.config.ui.max_displayed=Applications displayed +core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray +core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available +core.config.ui.tray.updates_icon=Update icon +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +core.config.updates.interval=Updates interval +custom_action.proceed_with=Proceed with {} ? +description=description +details=details +devices=devices +do_not.install=don't install +downgraded=downgraded +download=download +downloading=Downloading +error=error +example.short=e.g +exit=exit +file=file +file_chooser.title=File selector +files=files +finishing=finishing +gem_selector.question=What types of applications do you want to find here ? +gem_selector.title=Application types +general=general +icon_button.tooltip.disabled=This action is unavailable +imported=imported +initialization=initialization +install=install +installation=installation +installed=installed +internet.required=Internet connection is required +interval=interval +latest_version=latest version +license=license locale.ca=catalan locale.de=german +locale.en=english +locale.es=spanish locale.it=italian +locale.pt=portuguese locale.ru=russian -interval=interval -installation=installation -download=download -clean=clean -action.update.status.sorting=Determining the best update order -action.update.requirements.title=Upgrade requirements -action.update.requirements.body=The following {} dependencies must be installed before upgrading -action.update.install_req.fail.title=Installation failed -action.update.install_req.fail.body=It was not possible to install {} -action.update.requirements.status=Checking requirements -action.update.order.title=Upgrade order -action.update.order.body=The upgrade will be performed in the following order \ No newline at end of file +manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ? +manage_window.apps_table.row.actions.downgrade=Downgrade +manage_window.apps_table.row.actions.history=History +manage_window.apps_table.row.actions.info=Information +manage_window.apps_table.row.actions.install.popup.body=Install {} on your device ? +manage_window.apps_table.row.actions.install.popup.title=Installation +manage_window.apps_table.row.actions.install=Install +manage_window.apps_table.row.actions.refresh=Refresh +manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} from your device ? +manage_window.apps_table.row.actions.uninstall.popup.title=Uninstall +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.upgrade_toggle.disabled.tooltip=But it is not possible to upgrade it. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Click here to check or uncheck the update. +manage_window.apps_table.upgrade_toggle.tooltip=There is an update for this application. +manage_window.bt.installed.text=installed +manage_window.bt.installed.tooltip=Click here to show the installed applications +manage_window.bt.refresh.text=Refresh +manage_window.bt.refresh.tooltip=Reload the data about installed applications +manage_window.bt.suggestions.text=Suggestions +manage_window.bt.suggestions.tooltip=Click here to show some applications suggestions +manage_window.bt.upgrade.text=Upgrade +manage_window.bt.upgrade.tooltip=Upgrade all checked applications +manage_window.bt_custom_actions.tip=Click here to show more actions +manage_window.bt_settings.tooltip=Click here to display the settings +manage_window.checkbox.only_apps=Apps +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.label.updates=Updates +manage_window.name_filter.placeholder=Filter by name +manage_window.name_filter.tooltip=Type here to filter apps by name +manage_window.settings.about=About +manage_window.status.downgrading=Downgrading +manage_window.status.filtering=Filtering +manage_window.status.history=Retrieving history +manage_window.status.info=Retrieving information +manage_window.status.installed=Loading installed +manage_window.status.installing=Installing +manage_window.status.refreshing=Refreshing +manage_window.status.refreshing_app=Refreshing +manage_window.status.running_app=Launching {} +manage_window.status.screenshots=Retrieving {} screenshots +manage_window.status.searching=Searching +manage_window.status.suggestions=Looking for suggestions +manage_window.status.uninstalling=Uninstalling +manage_window.status.upgrading=Upgrading +manage_window.title=my applications +manage_window.upgrade_all.popup.body=Upgrade all selected applications ? +manage_window.upgrade_all.popup.title=Upgrade +message.file.not_exist.body=The file {} seems not to exist +message.file.not_exist=File does not exist +mirror=mirror +name=name +no=no +not_installed=not installed +notification.downgrade.failed=Failed to downgrade +notification.install.failed=installation failed +notification.uninstall.failed=uninstallation failed +notification.update_selected.failed=Update failed +notification.update_selected.success=app(s) updated successfully +ok=ok +others=others +popup.button.cancel=Cancel +popup.button.no=No +popup.button.yes=Yes +popup.history.selected.tooltip=Current version +popup.history.title=History +popup.root.bad_password.body=Wrong password +popup.root.bad_password.last_try=Attempts finished. Action cancelled. +popup.root.bad_password.title=Error +popup.root.continue=continue +popup.root.title=Requires your password to continue +popup.screenshots.no_screenshot.body=it was not possible to retrieve {} screenshots +prepare_panel.bt_skip.label=skip +prepare_panel.title.finish=Ready +prepare_panel.title.start=Initializing +proceed=proceed +publisher.verified=verified +publisher=publisher +repository=repository +screenshots.bt_back.label=previous +screenshots.bt_next.label=next +screenshots.download.no_content=No content to display +screenshots.download.no_response=Image not found +screenshots.download.running=downloading image +settings.changed.success.reboot=Restart now ? +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.error=It was not possible to properly change all the settings +settings=settings +show=show +size=size +source=source +stable=stable +status.caching_data=Caching {} data to disk +style=style +success=success +summary=summary +task.download_categories=Downloading categories [ {} ] +type=type +uninstall=uninstall +uninstalled=uninstalled +unknown=unknown +updates=updates +user=user +version.installed=installed version +version.installed_outdated=the installed version is outdated +version.latest=latest version +version.outdated=outdated +version.unknown=not informed +version.updated=updated +version=version +view.components.file_chooser.placeholder=Click here to select +warning=warning +welcome=welcome +warning.update_available=There is a new {} update available ({}). Checkout the news at {}. +window_manage.input_search.placeholder=Search +window_manage.input_search.tooltip=Type and press ENTER to search for applications +yes=yes \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index 526a4d56..c65d4b92 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -1,246 +1,217 @@ -manage_window.title=Mis aplicaciones -manage_window.columns.latest_version=Versión más reciente -manage_window.columns.update=¿Quiere actualizar? -manage_window.columns.installed=Instaladas -manage_window.apps_table.row.actions.info=Información -manage_window.apps_table.row.actions.history=Histórico -manage_window.apps_table.row.actions.uninstall=Desinstalar -manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación -manage_window.apps_table.row.actions.uninstall.popup.body=¿Quiere desinstalar {} del equipo? -manage_window.apps_table.row.actions.install.popup.title=Instalación -manage_window.apps_table.row.actions.install.popup.body=¿Quiere instalar {} en el equipo? -manage_window.apps_table.row.actions.downgrade=Revertir versión -manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quiere revertir la versión actual de {}? -manage_window.apps_table.row.actions.install=Instalar -manage_window.apps_table.row.actions.refresh=Actualizar -manage_window.apps_table.upgrade_toggle.tooltip=Pulse aquí para marcar o desmarcar la actualización -manage_window.checkbox.only_apps=Aplicaciones -window_manage.input_search.placeholder=Buscar -window_manage.input_search.tooltip=Escriba y oprima Intro para buscar aplicaciones -manage_window.label.updates=Actualizaciones -manage_window.status.refreshing=Recargando -manage_window.status.upgrading=Actualizando -manage_window.status.uninstalling=Desinstalando -manage_window.status.downgrading=Revirtiendo -manage_window.status.info=Obteniendo información -manage_window.status.history=Obteniendo el histórico -manage_window.status.searching=Buscando -manage_window.status.installing=Instalando -manage_window.status.refreshing_app=Actualizando -manage_window.status.running_app=Ejecutando {} -manage_window.status.filtering=Filtrando -manage_window.status.suggestions=Buscando sugerencias -manage_window.bt.refresh.text=Recargar -manage_window.bt.refresh.tooltip=Recarga los datos sobre las aplicaciones instaladas -manage_window.bt.upgrade.text=Actualizar -manage_window.bt.upgrade.tooltip=Actualiza todas las aplicaciones marcadas -manage_window.bt.installed.text=instaladas -manage_window.bt.installed.tooltip==Pulse aquí para mostrar las aplicaciones instaladas -manage_window.bt.suggestions.text=Sugerencias -manage_window.bt.suggestions.tooltip=Pulse aquí para mostrar algunas sugerencias de aplicaciones -manage_window.checkbox.show_details=Mostrar detalles -popup.root.title=Proporcione su contraseña para continuar -popup.root.continue=continuar -popup.root.bad_password.title=Error -popup.root.bad_password.body=Contraseña incorrecta -popup.root.bad_password.last_try=Intentos finalizados. Acción cancelada. -popup.history.title=Histórico -popup.history.selected.tooltip=Versión actual -popup.button.yes=Sí -popup.button.no=No -popup.button.cancel=Cancelar -tray.action.manage=Aplicaciones -tray.action.exit=Cerrar -tray.action.about=Acerca de -tray.action.refreshing=Recargando -action.info.tooltip=Pulse aquí para ver información sobre esta aplicación -action.settings.tooltip=Pulse aquí para abrir acciones adicionales -notification.update={} actualización disponible -notification.updates={} actualizaciones disponibles -notification.update_selected.success=aplicación(es) actualizada(s) correctamente -notification.update_selected.failed=La actualización ha fallado -notification.install.failed=la instalación ha fallado -notification.uninstall.failed=la desinstalación ha fallado -notification.downgrade.failed=Error al revertir la versión -about.info.desc=Interfaz grafica para administración de aplicaciones para Linux -about.info.link=Mas información en -about.info.license=Licencia gratuita -about.info.rate.question=¿Le gusta esta herramienta? -about.info.rate.answer=Dele una estrella en GitHub para fortalecer el proyecto -about.info.trouble.question=¿Problemas? -about.info.trouble.answer=Abra un issue en GitHub para que los desarrolladores puedan ayudarlo -yes=sí -no=no -version.updated=actualizada -version.outdated=desactualizada -name=nombre -version=versión -latest_version=versión más reciente -description=descripción -type=tipo -installed=instalada -uninstalled=desinstalada -not_installed=no instalada -downgraded=versión revertida -others=otros -internet.required=Se requiere conexión a internet -updates=actualizaciones -version.installed=versión instalada -version.latest=versión más reciente -version.installed_outdated=la versión instalada está desactualizada -version.unknown=versión no informada -app.name=nombre de la aplicación -warning=aviso -install=instalar -uninstall=desinstalar -bt.app_upgrade=Actualizar -bt.app_not_upgrade=No actualizar -manage_window.upgrade_all.popup.title=Actualizar -manage_window.upgrade_all.popup.body=¿Quiere actualizar todas las aplicaciones seleccionadas? -manage_window.settings.about=Acerca de -confirmation=confirmación -and=y -error=error +Australia=Australia +Austria=Austria +Bangladesh=Bangladesh +Belarus=Bielorrusia +Belgium=Bélgica +Brazil=Brasil +Bulgaria=Bulgaria +Canada=Canadá +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=República Checa +Denmark=Dinamarca +Ecuador=Ecuador +France=Francia +Georgia=Georgia +Germany=Alemania +Greece=Grecia +Hong Kong=Hong Kong +Hungary=Hungría +Iceland=Islandia +India=India +Indonesia=Indonesia +Iran=Irán +Italy=Italy +Japan=Japón +Kenya=Kenya +Netherlands=Países Bajos +New Zealand=Nueva Zelanda +Norway=Noruega +Philippines=Filipinas +Poland=Polonia +Portugal=Portugal +Russia=Rusia +Singapore=Singapur +South Africa=Sudáfrica +South Korea=Corea del Sur +Spain=España +Sweden=Suecia +Switzerland=Suiza +Taiwan=Taiwán +Thailand=Tailandia +Turkey=Turquía +Ukraine=Ucrania +United Kingdom=Reino Unido +United States=Estados Unidos +action.backup.error.create=No fue posible generar una nueva copia de seguridad +action.backup.error.delete=No fue posible eliminar las copias de seguridad antiguas del sistema +action.backup.error.proceed=¿Continuar de todos modos? +action.backup.invalid_mode=Modo de copia de seguridad inválido +action.backup.msg=¿Desea generar una copia de seguridad del sistema antes de continuar? +action.backup.substatus.create=Generando una nueva copia de seguridad +action.backup.substatus.delete=Eliminando copias de seguridad antiguas action.cancelled=operación cancelada por el usuario -copy=copiar -back=volver -show=mostrar -ask.continue=¿Quiere continuar? -cancel=cancelar -status.caching_data=Almacenando en antememoria los datos de {} para el disco +action.disk_trim=Optimizando el disco ( TRIM ) +action.disk_trim.error=Hubo un problema al optimizar el disco +action.failed=Acción fallida +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history +action.info.tooltip=Pulse aquí para ver información sobre esta aplicación +action.not_allowed=Acción no permitida +action.request_reboot.title=Reinicio de sistema action.run.tooltip=Pulse aquí para iniciar la aplicación -any=cualquiera -manage_window.name_filter.tooltip=Escriba aquí para filtrar aplicaciones por nombre -manage_window.name_filter.placeholder=Filtrar por nombre -publisher=publicador -unknown=desconocido -welcome=bienvenido -gem_selector.title=Tipos de aplicaciones -gem_selector.question=¿Qué tipos de aplicaciones quiere encontrar aquí? -proceed=continuar -change=cambiar -exit=salir -style=estilo -manage_window.bt_settings.tooltip=Pulse aquí para exhibir las configuraciones -downloading=Descargando -console.install_logs.path=Los registros de instalación pueden encontrarse en {} -author=autor -source=origen -size=tamaño -category=categoría -categories=categorías -summary=resumen -manage_window.status.screenshots=Obteniendo imágenes de {} action.screenshots.tooltip=Pulse aquí para ver algunas fotos de esta aplicación -popup.screenshots.no_screenshot.body=No fue posible recuperar las fotos de {} -games=juegos -game=juego -videoeditor=editor de vídeos -browser=navegador -music=música -development=desarrollo -network=red -networks=redes -graphics=gráficos -system=sistema -utility=utilidad -terminalemulator=terminal -database=base de datos -databases=bases de datos -movies=películas -movie=película -library=biblioteca -books=libros -applications=aplicaciones -social=social -productivity=productividad -utilities=utilidades -photo=foto -video=vídeo -entertainment=entretenimiento -art=arte -design=diseño -reference=referencia -personalisation=personalización -education=educación -health=salud -fitness=condición física -science=ciencias -news=noticias -weather=tiempo -finance=finanzas -desktopsettings=configuraciones -texteditor=editor de texto -server=servidor -cloud=nube -instantmessaging=mensajería -messaging=mensajería -2dgraphics=gráficos 2d -vectorgraphics=gráficos vectoriales -office=oficina -devices=dispositivos -security=seguridad -audiovideo=audio y vídeo -audio=audio -amusement=diversión -webdevelopment=desarrollo web -filetransfer=transferencia de archivos -packagemanager=administrador de paquetes -sportsgame=juego de deportes -arcadegame=juego de máquina -player=jugador -fonts=tipos de letra -screenshots.bt_next.label=siguiente -screenshots.bt_back.label=anterior -screenshots,download.running=descargando imagen -screenshots.download.no_content=No hay contenido para mostrar -screenshots.download.no_response=No se encontró la imagen -continue=continuar -stable=estable -close=cerrar -publisher.verified=verificado -mirror=espejo -emulator=emulador -do_not.install=no instalar -repository=repositorio -details=detalles -communication=comunicación -administration=administración -settings=configuraciones +action.settings.tooltip=Pulse aquí para abrir acciones adicionales +action.update.cannot_update_label=No se puede actualizar +action.update.install_req.fail.body=No fue posible instalar {} +action.update.install_req.fail.title=Falló la instalación +action.update.label_to_remove=Necesitan ser desinstalados +action.update.label_to_upgrade=Serán actualizados +action.update.pkg.required_size=Requiere +action.update.required_label=Deben instalarse +action.update.required_size=Espacio en disco requerido (download) +action.update.requirements.body=Las siguientes {} dependencias deben ser instaladas antes de actualizar +action.update.requirements.status=Verificando los requisitos +action.update.requirements.title=Requisitos de actualización +action.update.status.checking_sizes=Calculando el tamaño de las actualizaciónes +action.update.success.reboot.line1=¡Sistema actualizado con éxito! +action.update.success.reboot.line2=Algunos cambios pueden requerir un reinicio del sistema para que surta efecto. +action.update.success.reboot.line3=¿Reiniciar ahora? +action.update.summary=Resumen de actualización +action.update.total_size=Tamaño de la actualización +action.trim_disk.ask=¿Optimizar el disco (trim)? address=dirección -view.components.file_chooser.placeholder=Pulse aquí para seleccionar -files=archivos all_files=todos los archivos -file_chooser.title=Selector de archivos -message.file.not_exist=Archivo no existe -message.file.not_exist.body=El archivo {} parece no existir -icon_button.tooltip.disabled=This action is unavailable -user=usuario -example.short=p.ej -core.config.tab.advanced=Avanzadas -core.config.tab.general=Generales -core.config.tab.ui=Interfaz -core.config.tab.tray=Bandeja -core.config.tab.types=Tipos -core.config.locale.label=idioma -core.config.disk_cache=Cache de disco -core.config.disk_cache.tip=Algunos datos sobre las aplicaciones instaladas serán almacenados en el disco para que puedan cargarse rápidamente en las siguientes inicializaciones -core.config.updates.interval=Intervalo de actualizaciones -core.config.updates.interval.tip=Define el intervalo de tiempo en el que se realizará la verificación de actualizaciones para las aplicaciones instaladas ( en SEGUNDOS ) -core.config.downloads=downloads +amount=cantidad +and=y +any=cualquiera +app.name=nombre de la aplicación +applications=applications +ask=preguntar +ask.continue=¿Quiere continuar? +author=autor +back=volver +bt.app_not_upgrade=No actualizar +bt.app_upgrade=Actualizar +bt.not_now=Ahora no +cancel=cancelar +categories=categorías +category=categoría +category.2dgraphics=gráficos 2d +category.administration=administración +category.amusement=diversión +category.applications=aplicaciones +category.arcadegame=juego de máquina +category.art=arte +category.audio=audio +category.audiovideo=audio y vídeo +category.books=libros +category.browser=navegador +category.cloud=nube +category.communication=comunicación +category.database=base de datos +category.databases=bases de datos +category.design=diseño +category.desktopsettings=configuraciones +category.development=desarrollo +category.education=educación +category.emulator=emulador +category.entertainment=entretenimiento +category.filetransfer=transferencia de archivos +category.finance=finanzas +category.fitness=condición física +category.fonts=tipos de letra +category.game=juego +category.games=juegos +category.graphics=gráficos +category.health=salud +category.instantmessaging=mensajería +category.library=biblioteca +category.media=media +category.messaging=mensajería +category.movie=película +category.movies=películas +category.music=música +category.network=red +category.networks=redes +category.news=noticias +category.none=ninguna +category.office=oficina +category.packagemanager=administrador de paquetes +category.personalisation=personalización +category.photo=foto +category.player=jugador +category.productivity=productividad +category.reference=referencia +category.science=ciencias +category.security=seguridad +category.server=servidor +category.settings=configuraciones +category.social=social +category.sportsgame=juego de deportes +category.system=sistema +category.terminalemulator=terminal +category.text editor=text editor +category.texteditor=editor de texto +category.utilities=utilidades +category.utility=utilidad +category.vectorgraphics=gráficos vectoriales +category.video=vídeo +category.videoeditor=editor de vídeos +category.viewer=viewer +category.weather=tiempo +category.web=web +category.webdevelopment=desarrollo web +change=cambiar +clean=limpiar +close=cerrar +confirmation=confirmación +console.install_logs.path=Los registros de instalación pueden encontrarse en {} +continue=continuar +copy=copiar +core.config.backup=Habilitada +core.config.backup.downgrade=Antes de revertir versión +core.config.backup.install=Antes de instalar +core.config.backup.mode=Modo +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.incremental.tip=Se generará una nueva copia de seguridad del sistema que contenga solo los archivos modificados desde la última copia. +core.config.backup.mode.only_one=Única +core.config.backup.mode.only_one.tip=Solo se guardará una copia de seguridad del sistema. Las copias preexistentes serán borradas. +core.config.backup.uninstall=Antes de desinstalar +core.config.backup.upgrade=Antes de actualizar core.config.download.icons=Descargar iconos core.config.download.icons.tip=Si los íconos de las aplicaciones se deben descargar y mostrar en la tabla core.config.download.multithreaded=Descarga segmentada core.config.download.multithreaded.tip=Si las aplicaciones, paquetes y archivos deben descargarse con una herramienta que usa segmentación / threads ( más rápido ). Por el momento, esta configuración solo funcionará si el paquete aria2 esté instalado +core.config.downloads=downloads +core.config.locale.label=idioma core.config.mem_cache=Almacenamiento de memoria core.config.mem_cache.data_exp=Expiración de datos core.config.mem_cache.data_exp.tip=Define la vida útil de los datos en memoria ( en SEGUNDOS ) core.config.mem_cache.icon_exp=Expiración de íconos core.config.mem_cache.icon_exp.tip=Define la vida útil de los íconos en memoria ( en SEGUNDOS ) +core.config.store_password=Solicitud de contraseña única +core.config.store_password.tip=Solicita la contraseña de usuario root solo una vez core.config.suggestions.activated=Suggestions core.config.suggestions.activated.tip=Si se pueden sugerir nuevas aplicaciones para instalación +core.config.suggestions.by_type=Sugerencias por tipo +core.config.suggestions.by_type.tip=Número máximo de sugerencias que deberían mostrarse por tipo de aplicación +core.config.system.dep_checking=Verificación única de sistema +core.config.system.dep_checking.tip=Si la verificación de disponibilidad de las tecnologías de su sistema debe ocurrir solo una vez core.config.system.notifications=Notificaciones core.config.system.notifications.tip=Si notificaciones deben mostrarse cuando finaliza una acción o hay actualizaciones +core.config.tab.advanced=Avanzadas +core.config.tab.backup=Copia de Seguridad +core.config.tab.general=Generales +core.config.tab.tray=Bandeja +core.config.tab.types=Tipos +core.config.tab.ui=Interfaz +core.config.trim_after_update=Optimizar disco después de actualizar +core.config.trim_after_update.tip=Optimiza el disco después de una actualización exitosa usando "trim". No habilite esta opción si su disco no es un SSD que permita trim. +core.config.types.tip=Marque los tipos de aplicaciones que desea administrar +core.config.ui.auto_scale=Escala automática +core.config.ui.auto_scale.tip=Activa el factor de escala de pantalla automática ( {} ). Soluciona problemas de escala para algunos ambientes desktop. core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=Si se deben activar las mejoras relacionadas con las resoluciones HDPI core.config.ui.max_displayed=Aplicaciones mostradas @@ -249,36 +220,159 @@ core.config.ui.tray.default_icon=Ícono predeterminado core.config.ui.tray.default_icon.tip=El icono predeterminado que se muestra en la bandeja core.config.ui.tray.updates_icon=Icono de actualización core.config.ui.tray.updates_icon.tip=El icono que se muestra cuando hay actualizaciones disponibles -core.config.system.dep_checking=Verificación única de sistema -core.config.system.dep_checking.tip=Si la verificación de disponibilidad de las tecnologías de su sistema debe ocurrir solo una vez -core.config.suggestions.by_type=Sugerencias por tipo -core.config.suggestions.by_type.tip=Número máximo de sugerencias que deberían mostrarse por tipo de aplicación -core.config.types.tip=Marque los tipos de aplicaciones que desea administrar -core.config.ui.auto_scale=Escala automática -core.config.ui.auto_scale.tip=Activa el factor de escala de pantalla automática ( {} ). Soluciona problemas de escala para algunos ambientes desktop. -core.config.updates.sort_pkgs=Ordenar actualizaciones -core.config.updates.sort_pkgs.tip=Define el mejor orden de actualización para las aplicaciones / paquetes seleccionados para evitar problemas -core.config.updates.dep_check=Mostrar requisitos de actualización -core.config.updates.dep_check.tip=Muestra todos las dependencias que deben instalarse antes de comenzar a actualizar los seleccionados -settings.changed.success.warning=Las configuraciones se cambiaron con éxito. Algunas solo tendrán efecto después del reinicio. -settings.changed.success.reboot=¿Reiniciar ahora? -settings.error=No fue posible cambiar correctamente todas las configuraciones -locale.es=inglés -locale.es=español -locale.pt=portugués +core.config.updates.interval=Intervalo de actualizaciones +core.config.updates.interval.tip=Define el intervalo de tiempo en el que se realizará la verificación de actualizaciones para las aplicaciones instaladas ( en SEGUNDOS ) +custom_action.proceed_with=¿Continuar con {}? +description=descripción +details=detalles +devices=dispositivos +do_not.install=no instalar +downgraded=versión revertida +download=descarga +downloading=Descargando +error=error +example.short=p.ej +exit=salir +file=archivo +file_chooser.title=Selector de archivos +files=archivos +finishing=finalizando +gem_selector.question=¿Qué tipos de aplicaciones quiere encontrar aquí? +gem_selector.title=Tipos de aplicaciones +general=general +icon_button.tooltip.disabled=This action is unavailable +imported=importado +initialization=inicialización +install=instalar +installation=instalación +installed=instalada +internet.required=Se requiere conexión a internet +interval=intervalo +latest_version=versión más reciente +license=licencia locale.ca=catalán locale.de=alemán +locale.en=inglés +locale.es=español locale.it=italiano +locale.pt=portugués locale.ru=ruso -interval=intervalo -installation=instalación -download=descarga -clean=limpiar -action.update.status.sorting=Determinando el mejor orden de actualización -action.update.requirements.title=Requisitos de actualización -action.update.requirements.body=Las siguientes {} dependencias deben ser instaladas antes de actualizar -action.update.install_req.fail.title=Falló la instalación -action.update.install_req.fail.body=No fue posible instalar {} -action.update.requirements.status=Verificando los requisitos -action.update.order.title=Orden de actualización -action.update.order.body=La actualización se realizará en el siguiente orden \ No newline at end of file +manage_window.apps_table.row.actions.downgrade=Revertir versión +manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quiere revertir la versión actual de {}? +manage_window.apps_table.row.actions.history=Histórico +manage_window.apps_table.row.actions.info=Información +manage_window.apps_table.row.actions.install=Instalar +manage_window.apps_table.row.actions.install.popup.body=¿Quiere instalar {} en el equipo? +manage_window.apps_table.row.actions.install.popup.title=Instalación +manage_window.apps_table.row.actions.refresh=Actualizar +manage_window.apps_table.row.actions.uninstall=Desinstalar +manage_window.apps_table.row.actions.uninstall.popup.body=¿Quiere desinstalar {} del equipo? +manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación +manage_window.apps_table.upgrade_toggle.disabled.tooltip=Pero no es posible actualizarla. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Pulse aquí para marcar o desmarcar la actualización. +manage_window.apps_table.upgrade_toggle.tooltip=Hay una actualización para esta aplicación. +manage_window.bt.installed.text=instaladas +manage_window.bt.installed.tooltip=Pulse aquí para mostrar las aplicaciones instaladas +manage_window.bt.refresh.text=Recargar +manage_window.bt.refresh.tooltip=Recarga los datos sobre las aplicaciones instaladas +manage_window.bt.suggestions.text=Sugerencias +manage_window.bt.suggestions.tooltip=Pulse aquí para mostrar algunas sugerencias de aplicaciones +manage_window.bt.upgrade.text=Actualizar +manage_window.bt.upgrade.tooltip=Actualiza todas las aplicaciones marcadas +manage_window.bt_custom_actions.tip=Pulse aquí para mostrar más acciones +manage_window.bt_settings.tooltip=Pulse aquí para exhibir las configuraciones +manage_window.checkbox.only_apps=Aplicaciones +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.label.updates=Actualizaciones +manage_window.name_filter.placeholder=Filtrar por nombre +manage_window.name_filter.tooltip=Escriba aquí para filtrar aplicaciones por nombre +manage_window.settings.about=Acerca de +manage_window.status.downgrading=Revirtiendo +manage_window.status.filtering=Filtrando +manage_window.status.history=Obteniendo el histórico +manage_window.status.info=Obteniendo información +manage_window.status.installed=Cargando instalados +manage_window.status.installing=Instalando +manage_window.status.refreshing=Recargando +manage_window.status.refreshing_app=Actualizando +manage_window.status.running_app=Ejecutando {} +manage_window.status.screenshots=Obteniendo imágenes de {} +manage_window.status.searching=Buscando +manage_window.status.suggestions=Buscando sugerencias +manage_window.status.uninstalling=Desinstalando +manage_window.status.upgrading=Actualizando +manage_window.title=Mis aplicaciones +manage_window.upgrade_all.popup.body=¿Quiere actualizar todas las aplicaciones seleccionadas? +manage_window.upgrade_all.popup.title=Actualizar +message.file.not_exist=Archivo no existe +message.file.not_exist.body=El archivo {} parece no existir +mirror=espejo +name=nombre +no=no +not_installed=no instalada +notification.downgrade.failed=Error al revertir la versión +notification.install.failed=la instalación ha fallado +notification.uninstall.failed=la desinstalación ha fallado +notification.update_selected.failed=La actualización ha fallado +notification.update_selected.success=aplicación(es) actualizada(s) correctamente +ok=ok +others=otros +popup.button.cancel=Cancelar +popup.button.no=No +popup.button.yes=Sí +popup.history.selected.tooltip=Versión actual +popup.history.title=Histórico +popup.root.bad_password.body=Contraseña incorrecta +popup.root.bad_password.last_try=Intentos finalizados. Acción cancelada. +popup.root.bad_password.title=Error +popup.root.continue=continuar +popup.root.title=Proporcione su contraseña para continuar +popup.screenshots.no_screenshot.body=No fue posible recuperar las fotos de {} +prepare_panel.bt_skip.label=omitir +prepare_panel.title.finish=Listo +prepare_panel.title.start=Inicializando +proceed=continuar +publisher=publicador +publisher.verified=verificado +repository=repositorio +screenshots.bt_back.label=anterior +screenshots.bt_next.label=siguiente +screenshots.download.no_content=No hay contenido para mostrar +screenshots.download.no_response=No se encontró la imagen +screenshots.download.running=descargando imagen +settings=configuraciones +settings.changed.success.reboot=¿Reiniciar ahora? +settings.changed.success.warning=Las configuraciones se cambiaron con éxito. Algunas solo tendrán efecto después del reinicio. +settings.error=No fue posible cambiar correctamente todas las configuraciones +show=mostrar +size=tamaño +source=origen +stable=estable +status.caching_data=Almacenando en antememoria los datos de {} para el disco +style=estilo +success=éxito +summary=resumen +task.download_categories=Descargando categorías [ {} ] +type=tipo +uninstall=desinstalar +uninstalled=desinstalada +unknown=desconocido +updates=actualizaciones +user=usuario +version=versión +version.installed=versión instalada +version.installed_outdated=la versión instalada está desactualizada +version.latest=versión más reciente +version.outdated=desactualizada +version.unknown=versión no informada +version.updated=actualizada +view.components.file_chooser.placeholder=Pulse aquí para seleccionar +warning=aviso +warning.update_available=Hay una nueva actualización de {} disponible ({}). Vea las novedades en {}. +welcome=bienvenido +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Escriba y oprima Intro para buscar aplicaciones +yes=sí \ No newline at end of file diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 4f8b104c..08db453a 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -1,236 +1,380 @@ -manage_window.title=Le mie applicazioni -manage_window.columns.latest_version=Ultima Versione -manage_window.columns.update=Upgrade ? -manage_window.apps_table.row.actions.info=Informazioni -manage_window.apps_table.row.actions.history=Cronologia -manage_window.apps_table.row.actions.uninstall.popup.title=Disinstalla -manage_window.apps_table.row.actions.uninstall.popup.body=Remuovi {} dal tuo dispositivo ? -manage_window.apps_table.row.actions.install.popup.title=Installazione -manage_window.apps_table.row.actions.install.popup.body=Installa {} nel tuo dispositivo ? -manage_window.apps_table.row.actions.downgrade=Downgrade -manage_window.apps_table.row.actions.downgrade.popup.body=Vuoi veramente fare il downgrade {} ? -manage_window.apps_table.row.actions.install=Installa -manage_window.apps_table.row.actions.refresh=Ricarica -manage_window.apps_table.upgrade_toggle.tooltip=C'è un aggiornamento per questa applicazione. Fai clic qui per selezionare o deselezionare l'aggiornamento -manage_window.checkbox.only_apps=Apps -window_manage.input_search.placeholder=Cerca -window_manage.input_search.tooltip=Digitare e premere INVIO per cercare applicazioni -manage_window.label.updates=Aggiornamenti -manage_window.status.refreshing=Riaggiornamento -manage_window.status.upgrading=Aggiornamento -manage_window.status.uninstalling=Disinstallare -manage_window.status.downgrading=Downgrading -manage_window.status.info=Recupero di informazioni -manage_window.status.history=Recupero della cronologia -manage_window.status.searching=Ricerca -manage_window.status.installing=Installazione -manage_window.status.refreshing_app=Refreshing -manage_window.status.running_app=Avvio {} -manage_window.status.filtering=Filtraggio -manage_window.status.screenshots=Recupero {} schermate -manage_window.status.suggestions=Cercando suggerimenti -manage_window.bt.refresh.text=Refresh -manage_window.bt.refresh.tooltip=Ricarica i dati sulle applicazioni installate -manage_window.bt.upgrade.text=Aggiorna -manage_window.bt.upgrade.tooltip=Aggiorna tutte le applicazioni controllate -manage_window.bt.installed.text=installato -manage_window.bt.installed.tooltip=Clicca qui per mostrare le applicazioni installate -manage_window.bt.suggestions.text=Suggerimenti -manage_window.bt.suggestions.tooltip=Clicca qui per mostrare alcuni suggerimenti sulle applicazioni -manage_window.checkbox.show_details=Mostra dettagli -popup.root.title=Richiede la tua password per continuare -popup.root.continue=continuare -popup.root.bad_password.title=Errore -popup.root.bad_password.body=Password errata -popup.root.bad_password.last_try=Tentativi conclusi. Azione annullata. -popup.history.title=Cronlogia -popup.history.selected.tooltip=Version corrente -popup.button.yes=Si -popup.button.no=No -popup.button.cancel=Cancella -tray.action.manage=Applicazioni -tray.action.exit=Abbandona -tray.action.about=Informazioni su -tray.action.refreshing=Refreshing +Australia=Australia +Austria=Austria +Bangladesh=Bangladesh +Belarus=Belarus +Belgium=Belgium +Brazil=Brazil +Bulgaria=Bulgaria +Canada=Canada +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=Czech +Denmark=Denmark +Ecuador=Ecuador +France=France +Georgia=Georgia +Germany=Germany +Greece=Greece +Hong Kong=Hong Kong +Hungary=Hungary +Iceland=Iceland +India=India +Indonesia=Indonesia +Iran=Iran +Italy=Italy +Japan=Japan +Kenya=Kenya +Netherlands=Netherlands +New Zealand=New Zealand +Norway=Norway +Philippines=Philippines +Poland=Poland +Portugal=Portugal +Russia=Russia +Singapore=Singapore +South Africa=South Africa +South Korea=South Korea +Spain=Spain +Sweden=Sweden +Switzerland=Switzerland +Taiwan=Taiwan +Thailand=Thailand +Turkey=Turkey +Ukraine=Ukraine +United Kingdom=United Kingdom +United States=United States +action.backup.error.create=It was not possible to generate a new system copy +action.backup.error.delete=It was not possible to delete the old system copies +action.backup.error.proceed=Proceed anyway ? +action.backup.invalid_mode=Invalid backup mode +action.backup.msg=Do you want to generate a system copy before proceeding ? +action.backup.substatus.create=Generating a new system backup +action.backup.substatus.delete=Removing old system backups +action.cancelled=operazione annullata dall'utente +action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim.error=There was a problem while optimizing the disk +action.failed=Azione non riuscita +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history action.info.tooltip=Clicca qui per vedere informazioni su questa applicazione +action.not_allowed=Action not allowed +action.request_reboot.title=System restart +action.run.tooltip=Fai clic qui per avviare questa applicazione action.screenshots.tooltip=Clicca qui per vedere alcune immagini di questa applicazione action.settings.tooltip=Fai clic qui per aprire ulteriori azioni -notification.update={} aggiornamento disponibile -notification.updates={} aggiornamenti disponibile -notification.update_selected.success=app aggiornate correttamente -notification.update_selected.failed=Aggiornamento non riuscito -notification.install.failed=installazione fallita -notification.uninstall.failed=disinstallazione non riuscita -notification.downgrade.failed=Impossibile eseguire il downgrade -about.info.desc=Interfaccia grafica per la gestione delle tue applicazioni Linux -about.info.link=Maggiori informazioni su -about.info.license=Free license -about.info.rate.question=Ti piace questo strumento? -about.info.rate.answer=Dagli una stella su GitHub per rafforzare il progetto -about.info.trouble.question=Problemas? -about.info.trouble.answer=Apri un issue in GitHub in modo che gli sviluppatori possano aiutarti -yes=si -no=no -version.updated=aggiornata -version.outdated=obsoleta -name=nome -version=versione -latest_version=ultima versione -description=descrizione -type=tipe -installed=installato -uninstalled=disinstallato -not_installed=non installato -downgraded=downgraded -others=altre -internet.required=È richiesta una connessione a Internet -updates=aggiornamenti -version.installed=versione installata -version.latest=ultima versione -version.installed_outdated=la versione installata è obsoleta -version.unknown=non informato -app.name=Name applicazione -warning=Avviso -install=installa -uninstall=Disinstalla -bt.app_upgrade=Upgrade -bt.app_not_upgrade=Non aggiornare -manage_window.upgrade_all.popup.title=Aggiorna -manage_window.upgrade_all.popup.body=Aggiornare tutte le applicazioni selezionate ? -manage_window.settings.about=Informazione su -confirmation=conferma -and=e -error=errore -action.cancelled=operazione annullata dall'utente -copy=copia -back=indietro -show=mostra -ask.continue=Continuare ? -cancel=cancella -status.caching_data=Memorizza {} dati sul disco -action.run.tooltip=Fai clic qui per avviare questa applicazione -any=qualunque -manage_window.name_filter.tooltip=Digita qui per filtrare le app per nome -manage_window.name_filter.placeholder=Filtra per nome -publisher=editore -unknown=sconosciuto -welcome=benvenuto -gem_selector.title=Tipi di applicazione -gem_selector.question=Quali tipi di applicazioni vuoi trovare qui ? -proceed=procedere -change=Cambia -exit=esci -style=stile -manage_window.bt_settings.tooltip=Fai clic qui per visualizzare le impostazioni -downloading=Scaricamento -console.install_logs.path=I registri di installazione sono disponibili all'indirizzo {} -author=autore -source=sorgente -size=dimensione -category=categoria -categories=categorie -summary=riepilogo -popup.screenshots.no_screenshot.body=non è stato possibile recuperare {} schermate -terminalemulator=terminale -desktopsettings=impostazioni del desktop -texteditor=editor di testo -screenshots.bt_next.label=prossimo -screenshots.bt_back.label=precedente -instantmessaging=messaggistica -messaging=messaggistica -2dgraphics=Grafica 2D -vectorgraphics=grafica vettoriale -audiovideo=audio / video -audio=audio -webdevelopment=sviluppo web -filetransfer=trasferimento di file -packagemanager=gestore pacchetti -sportsgame=gioco di sport -arcadegame=Gioco arcade -player=giocatore -screenshots,download.running=scaricare l'immagine -screenshots.download.no_content=Nessun contenuto da visualizzare -screenshots.download.no_response=Immagine non trovata -continue=continua -stable=stabile -close=vicino -publisher.verified=verificato -mirror=specchio -emulator=emulatore -do_not.install=non installare -repository=deposito -details=dettagli -communication=comunicazione -administration=amministrazione -settings=impostazioni +action.update.cannot_update_label=Cannot be upgraded +action.update.install_req.fail.body=Non è stato possibile installare {} +action.update.install_req.fail.title=Installazione non riuscita +action.update.label_to_remove=Must be uninstalled +action.update.label_to_upgrade=Will be upgraded +action.update.pkg.required_size=Requires +action.update.required_label=Must be installed +action.update.required_size=Disk space required (download) +action.update.requirements.body=Le seguenti {} dipendenze devono essere installati prima dell'aggiornamento +action.update.requirements.status=Verificando i requisiti +action.update.requirements.title=Requisiti di aggiornamento +action.update.status.checking_sizes=Calculating upgrade size +action.update.success.reboot.line1=Upgrades successfully applied ! +action.update.success.reboot.line2=Some changes may require a system restart to take effect. +action.update.success.reboot.line3=Restart now ? +action.update.summary=Upgrade summary +action.update.total_size=Update size +action.trim_disk.ask=Optimize disc (trim) ? address=indirizzo -view.components.file_chooser.placeholder=Fai clic qui per selezionare -files=files all_files=tutti i files -file_chooser.title=Selettore file -message.file.not_exist=File non esiste -message.file.not_exist.body=Il file {} sembra non esistere -development=sviluppo -icon_button.tooltip.disabled=Questa azione non è disponibile -user=utente -example.short=e.g -core.config.tab.advanced=Advanced -core.config.tab_label=general -core.config.tab.ui=Interface -core.config.tab.types=Types -core.config.locale.label=language -core.config.disk_cache=Disk cache -core.config.disk_cache.tip=Some data about the installed applications will be stored into the disk so they can be quickly loaded in the next initializations -core.config.updates.interval=Updates interval -core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) -core.config.downloads=downloads +amount=amount +and=e +any=qualunque +app.name=Name applicazione +applications=applications +ask=ask +ask.continue=Continuare ? +author=autore +back=indietro +bt.app_not_upgrade=Non aggiornare +bt.app_upgrade=Upgrade +bt.not_now=Not now +cancel=cancella +categories=categorie +category=categoria +category.2dgraphics=Grafica 2D +category.administration=amministrazione +category.amusement=amusement +category.arcadegame=Gioco arcade +category.art=art +category.audio=audio +category.audiovideo=audio / video +category.books=books +category.browser=browser +category.cloud=cloud +category.communication=comunicazione +category.database=database +category.databases=databases +category.design=design +category.desktopsettings=impostazioni del desktop +category.development=development +category.education=education +category.emulator=emulatore +category.entertainment=entertainment +category.filetransfer=trasferimento di file +category.finance=finance +category.fitness=fitness +category.fonts=fonts +category.game=game +category.games=games +category.graphics=graphics +category.health=health +category.instantmessaging=messaggistica +category.library=library +category.media=media +category.messaging=messaggistica +category.movie=movie +category.movies=movies +category.music=music +category.network=network +category.networks=networks +category.news=news +category.none=None +category.office=office +category.packagemanager=gestore pacchetti +category.personalisation=personalization +category.photo=photo +category.player=giocatore +category.productivity=productivity +category.reference=reference +category.science=science +category.security=security +category.server=server +category.settings=impostazioni +category.social=social +category.sportsgame=gioco di sport +category.system=system +category.terminalemulator=terminale +category.text editor=text editor +category.texteditor=editor di testo +category.utilities=utilities +category.utility=utility +category.vectorgraphics=grafica vettoriale +category.video=video +category.videoeditor=video editor +category.viewer=viewer +category.weather=weather +category.web=web +category.webdevelopment=sviluppo web +change=Cambia +clean=pulire +close=vicino +confirmation=conferma +console.install_logs.path=I registri di installazione sono disponibili all'indirizzo {} +continue=continua +copy=copia +core.config.backup=Enabled +core.config.backup.downgrade=Before downgrading +core.config.backup.install=Before installing +core.config.backup.mode=Mode +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy. +core.config.backup.mode.only_one=Single +core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased. +core.config.backup.uninstall=Before uninstalling +core.config.backup.upgrade=Before upgrading core.config.download.icons=Download icons core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table core.config.download.multithreaded=Multithreaded download core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed +core.config.downloads=downloads +core.config.locale.label=language core.config.mem_cache=Memory storage core.config.mem_cache.data_exp=Data expiration core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS ) +core.config.mem_cache.icon_exp=Icons expiration +core.config.mem_cache.icon_exp.tip=Defines the in-memory icons lifetime ( in SECONDS ) +core.config.store_password=Single password request +core.config.store_password.tip=Requests the root user password only once core.config.suggestions.activated=Suggestions core.config.suggestions.activated.tip=If new applications can be suggested for installation +core.config.suggestions.by_type=Suggestions by type +core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type +core.config.system.dep_checking=Single system checking +core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once core.config.system.notifications=Notifications core.config.system.notifications.tip=If notifications should be displayed when an action is finished or there are updates +core.config.tab.advanced=Advanced +core.config.tab.backup=Backup +core.config.tab.general=General +core.config.tab.tray=Tray +core.config.tab.types=Types +core.config.tab.ui=Interface +core.config.tab_label=general +core.config.trim_after_update=Optimize disc after upgrading +core.config.trim_after_update.tip=It optimizes the disc after a successfull upgrade using "trim". Do not enable this option if your disk is not a SSD that allows trim. +core.config.types.tip=Check the application types you want to manage +core.config.ui.auto_scale=Auto scale +core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=If improvements related to HDPI resolutions should be activated core.config.ui.max_displayed=Applications displayed core.config.ui.max_displayed.tip=Maximum number of applications displayed on the table core.config.ui.tray.default_icon=Default icon +core.config.ui.tray.default_icon.tip=The default icon for {app} displayed on the tray core.config.ui.tray.updates_icon=Update icon -core.config.system.dep_checking=Single system checking -core.config.system.dep_checking.tip=If the availability checking of your system technologies should happen only once -core.config.suggestions.by_type=Suggestions by type -core.config.suggestions.by_type.tip=Maximum number of suggestions that should be displayed by application type -core.config.types.tip=Check the application types you want to manage -core.config.ui.auto_scale=Auto scale -core.config.ui.auto_scale.tip=It activates the auto screen scale factor ( {} ). It fixes scaling issues for some desktop environments. -core.config.updates.sort_pkgs=Sort updates -core.config.updates.sort_pkgs.tip=Defines the best update order for the selected applications / packages to avoid issues -core.config.updates.dep_check=Mostra i requisiti di aggiornamento -core.config.updates.dep_check.tip=Visualizza tutti i dipendenze aggiuntivi che devono essere installati prima di iniziare ad aggiornare quelli selezionati -settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. -settings.changed.success.reboot=Restart now ? -settings.error=It was not possible to properly change all the settings -locale.en=inglese -locale.es=spagnolo -locale.pt=portoghese +core.config.ui.tray.updates_icon.tip=The displayed icon when there are updates available +core.config.updates.interval=Updates interval +core.config.updates.interval.tip=Defines the time interval in which the update-checking for the installed applications will happen ( in SECONDS ) +custom_action.proceed_with=Proceed with {} ? +description=descrizione +details=dettagli +development=sviluppo +devices=devices +do_not.install=non installare +downgraded=downgraded +download=download +downloading=Scaricamento +error=errore +example.short=e.g +exit=esci +file=file +file_chooser.title=Selettore file +files=files +finishing=finishing +gem_selector.question=Quali tipi di applicazioni vuoi trovare qui ? +gem_selector.title=Tipi di applicazione +general=generale +icon_button.tooltip.disabled=Questa azione non è disponibile +imported=imported +initialization=inizializzazione +install=installa +installation=installazione +installed=installato +internet.required=È richiesta una connessione a Internet +interval=intervallo +latest_version=ultima versione +license=license locale.ca=catalan locale.de=tedesco +locale.en=inglese +locale.es=spagnolo locale.it=italiano +locale.pt=portoghese locale.ru=russo -interval=intervallo -installation=installazione -download=download -clean=pulire -action.update.status.sorting=Sto determinando il miglior ordine di aggiornamento -action.update.requirements.title=Requisiti di aggiornamento -action.update.requirements.body=Le seguenti {} dipendenze devono essere installati prima dell'aggiornamento -action.update.install_req.fail.title=Installazione non riuscita -action.update.install_req.fail.body=Non è stato possibile installare {} -action.update.requirements.status=Verificando i requisiti -action.update.order.title=Ordine di aggiornamento -action.update.order.body=L'aggiornamento verrà eseguito nel seguente ordine \ No newline at end of file +manage_window.apps_table.row.actions.downgrade=Downgrade +manage_window.apps_table.row.actions.downgrade.popup.body=Vuoi veramente fare il downgrade {} ? +manage_window.apps_table.row.actions.history=Cronologia +manage_window.apps_table.row.actions.info=Informazioni +manage_window.apps_table.row.actions.install=Installa +manage_window.apps_table.row.actions.install.popup.body=Installa {} nel tuo dispositivo ? +manage_window.apps_table.row.actions.install.popup.title=Installazione +manage_window.apps_table.row.actions.refresh=Ricarica +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.body=Remuovi {} dal tuo dispositivo ? +manage_window.apps_table.row.actions.uninstall.popup.title=Disinstalla +manage_window.apps_table.upgrade_toggle.disabled.tooltip=But it is not possible to upgrade it. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Fai clic qui per selezionare o deselezionare l'aggiornamento. +manage_window.apps_table.upgrade_toggle.tooltip=C'è un aggiornamento per questa applicazione. +manage_window.bt.installed.text=installato +manage_window.bt.installed.tooltip=Clicca qui per mostrare le applicazioni installate +manage_window.bt.refresh.text=Refresh +manage_window.bt.refresh.tooltip=Ricarica i dati sulle applicazioni installate +manage_window.bt.suggestions.text=Suggerimenti +manage_window.bt.suggestions.tooltip=Clicca qui per mostrare alcuni suggerimenti sulle applicazioni +manage_window.bt.upgrade.text=Aggiorna +manage_window.bt.upgrade.tooltip=Aggiorna tutte le applicazioni controllate +manage_window.bt_custom_actions.tip=Click here to show more actions +manage_window.bt_settings.tooltip=Fai clic qui per visualizzare le impostazioni +manage_window.checkbox.only_apps=Apps +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.label.updates=Aggiornamenti +manage_window.name_filter.placeholder=Filtra per nome +manage_window.name_filter.tooltip=Digita qui per filtrare le app per nome +manage_window.settings.about=Informazione su +manage_window.status.downgrading=Downgrading +manage_window.status.filtering=Filtraggio +manage_window.status.history=Recupero della cronologia +manage_window.status.info=Recupero di informazioni +manage_window.status.installed=Loading installed +manage_window.status.installing=Installazione +manage_window.status.refreshing=Riaggiornamento +manage_window.status.refreshing_app=Refreshing +manage_window.status.running_app=Avvio {} +manage_window.status.screenshots=Recupero {} schermate +manage_window.status.searching=Ricerca +manage_window.status.suggestions=Cercando suggerimenti +manage_window.status.uninstalling=Disinstallare +manage_window.status.upgrading=Aggiornamento +manage_window.title=Le mie applicazioni +manage_window.upgrade_all.popup.body=Aggiornare tutte le applicazioni selezionate ? +manage_window.upgrade_all.popup.title=Aggiorna +message.file.not_exist=File non esiste +message.file.not_exist.body=Il file {} sembra non esistere +mirror=specchio +name=nome +no=no +not_installed=non installato +notification.downgrade.failed=Impossibile eseguire il downgrade +notification.install.failed=installazione fallita +notification.uninstall.failed=disinstallazione non riuscita +notification.update_selected.failed=Aggiornamento non riuscito +notification.update_selected.success=app aggiornate correttamente +ok=ok +others=altre +popup.button.cancel=Cancella +popup.button.no=No +popup.button.yes=Si +popup.history.selected.tooltip=Version corrente +popup.history.title=Cronlogia +popup.root.bad_password.body=Password errata +popup.root.bad_password.last_try=Tentativi conclusi. Azione annullata. +popup.root.bad_password.title=Errore +popup.root.continue=continuare +popup.root.title=Richiede la tua password per continuare +popup.screenshots.no_screenshot.body=non è stato possibile recuperare {} schermate +prepar_panel.bt_skip.label=saltare +prepare_panel.bt_skip.label=skip +prepare_panel.title.finish=Pronto +prepare_panel.title.start=Inizializzazione +proceed=procedere +publisher=editore +publisher.verified=verificato +repository=deposito +screenshots.bt_back.label=precedente +screenshots.bt_next.label=prossimo +screenshots.download.no_content=Nessun contenuto da visualizzare +screenshots.download.no_response=Immagine non trovata +screenshots.download.running=scaricare l'immagine +settings=impostazioni +settings.changed.success.reboot=Restart now ? +settings.changed.success.warning=Settings successfully changed. Some of them will only take effect after a restart. +settings.error=It was not possible to properly change all the settings +show=mostra +size=dimensione +source=sorgente +stable=stabile +status.caching_data=Memorizza {} dati sul disco +style=stile +success=success +summary=riepilogo +task.download_categories=Download delle categorie [ {} ] +type=tipe +uninstall=Disinstalla +uninstalled=disinstallato +unknown=sconosciuto +updates=aggiornamenti +user=utente +version=versione +version.installed=versione installata +version.installed_outdated=la versione installata è obsoleta +version.latest=ultima versione +version.outdated=obsoleta +version.unknown=non informato +version.updated=aggiornata +view.components.file_chooser.placeholder=Fai clic qui per selezionare +warning=Avviso +warning.update_available=There is a new {} update available ({}). Checkout the news at {}. +welcome=benvenuto +window_manage.input_search.placeholder=Cerca +window_manage.input_search.tooltip=Digitare e premere INVIO per cercare applicazioni +yes=si \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index eb84e839..cccb1e1d 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -1,287 +1,377 @@ -manage_window.title=meus aplicativos -manage_window.columns.latest_version=Última Versão -manage_window.columns.update=Atualizar ? -manage_window.columns.installed=Instalado -manage_window.apps_table.row.actions.info=Informação -manage_window.apps_table.row.actions.history=Histórico -manage_window.apps_table.row.actions.uninstall=Desinstalar -manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação -manage_window.apps_table.row.actions.uninstall.popup.body=Remover {} do seu dispositivo ? -manage_window.apps_table.row.actions.install.popup.title=Instalação -manage_window.apps_table.row.actions.install.popup.body=Instalar {} no seu dispositivo ? -manage_window.apps_table.row.actions.downgrade=Reverter versão -manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ? -manage_window.apps_table.row.actions.install=Instalar -manage_window.apps_table.row.actions.refresh=Atualizar -manage_window.apps_table.upgrade_toggle.tooltip=Existe um atualização para esse aplicativo. Clique aqui para marcar ou desmarcar a atualização -manage_window.checkbox.only_apps=Aplicativos -window_manage.input_search.placeholder=Buscar -window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos -manage_window.label.updates=Atualizações -manage_window.status.refreshing=Recarregando -manage_window.status.upgrading=Atualizando -manage_window.status.uninstalling=Desinstalando -manage_window.status.downgrading=Revertendo -manage_window.status.info=Obtendo informação -manage_window.status.history=Obtendo histórico -manage_window.status.searching=Buscando -manage_window.status.installing=Instalando -manage_window.status.refreshing_app=Atualizando -manage_window.status.running_app=Iniciando {} -manage_window.status.filtering=Filtrando -manage_window.status.suggestions=Procurando sugestões -manage_window.bt.refresh.text=Recarregar -manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados -manage_window.bt.upgrade.text=Atualizar -manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos marcados -manage_window.bt.installed.text=instalados -manage_window.bt.installed.tooltip=Clique aqui para exibir os aplicativos instalados -manage_window.bt.suggestions.text=Sugestões -manage_window.bt.suggestions.tooltip=Clique aqui para exibir algumas sugestões de aplicativos -manage_window.checkbox.show_details=Mostrar detalhes -popup.root.title=Requer sua senha para prosseguir -popup.root.continue=prosseguir -popup.root.bad_password.title=Erro -popup.root.bad_password.body=Senha incorreta -popup.root.bad_password.last_try=Tentativas finalizadas. Ação cancelada. -popup.history.title=Histórico -popup.history.selected.tooltip=Versão atual -popup.button.yes=Sim -popup.button.no=Não -popup.button.cancel=Cancelar -tray.action.manage=Aplicativos -tray.action.exit=Fechar -tray.action.about=Sobre -tray.action.refreshing=Recarregando -notification.update={} atualização disponível -notification.updates={} atualizações disponíveis -notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso -notification.update_selected.failed=Erro ao atualizar -notification.install.failed=instalação falhou -notification.uninstall.failed=desinstalação falhou -notification.downgrade.failed=Erro ao reverter versão -about.info.desc=Interface gráfica para gerenciamento de aplicativos Linux -about.info.link=Mais informações em -about.info.license=Licença gratuita -about.info.rate.question=Gosta desta ferramenta ? -about.info.rate.answer=Dê a ela uma estrela no GitHub para fortalecer o projeto -about.info.trouble.question=Problemas ? -about.info.trouble.answer=Abra uma issue no GitHub para que os desenvolvedores possam ajudá-lo -yes=sim -no=não -version.updated=atualizada -version.outdated=desatualizada -name=nome -version=versão -latest_version=última versão -description=descrição -type=tipo -installed=instalado -uninstalled=desinstalado -not_installed=não instalado -downgraded=versão revertida -others=outros -internet.required=É necessário estar conectado a Internet -updates=atualizações -version.installed=versão instalada -version.latest=versão mais recente -version.installed_outdated=a versão instalada está desatualizada -version.unknown=versão não informada -app.name=nome do aplicativo -warning=aviso -install=instalar -uninstall=desinstalar -bt.app_upgrade=Atualizar -bt.app_not_upgrade=Não atualizar -manage_window.upgrade_all.popup.title=Atualizar -manage_window.upgrade_all.popup.body=Atualizar todos os aplicativos selecionados ? -manage_window.settings.about=Sobre -confirmation=confirmação -and=e -error=erro +Australia=Austrália +Austria=Áustria +Bangladesh=Bangladesh +Belarus=Bielorrússia +Belgium=Bélgica +Brazil=Brasil +Bulgaria=Bulgária +Canada=Canadá +Chile=Chile +China=China +Costa Rica=Costa Rica +Czech=República Tcheca +Denmark=Dinamarca +Ecuador=Equador +France=França +Georgia=Georgia +Germany=Alemanha +Greece=Grécia +Hong Kong=Hong Kong +Hungary=Hungria +Iceland=Islândia +India=Índia +Indonesia=Indonésia +Iran=Irã +Italy=Itália +Japan=Japão +Kenya=Quênia +Netherlands=Holanda +New Zealand=Nova Zelândia +Norway=Noriega +Philippines=Filipinas +Poland=Polônia +Portugal=Portugal +Russia=Rússia +Singapore=Singapura +South Africa=África do Sul +South Korea=Coréia do Sul +Spain=Espanha +Sweden=Suécia +Switzerland=Suíça +Taiwan=Taiwan +Thailand=Tailândia +Turkey=Turquia +Ukraine=Ucrânia +United Kingdom=Reino Unido +United States=Estados Unidos +action.backup.error.create=Não foi possível gerar uma nova cópia de segurança do sistema +action.backup.error.delete=Não foi possível remover as cópias de segurança antigas do sistema +action.backup.error.proceed=Continuar mesmo assim ? +action.backup.invalid_mode=Modo de cópia de segurança inválido +action.backup.msg=Você deseja gerar uma cópia de segurança do sistema antes de proceder ? +action.backup.substatus.create=Gerando uma nova cópia de segurança +action.backup.substatus.delete=Removendo cópias de segurança antigas action.cancelled=operação cancelada pelo usuário -copy=copiar -back=voltar -show=mostrar -ask.continue=Continuar ? -cancel=cancelar -status.caching_data=Cacheando dados de {} para o disco -action.run.tooltip=Clique aqui para iniciar esse aplicativo +action.disk_trim.error=Ocorreu um problema ao otimizar o disco +action.disk_trim=Otimizando disco ( TRIM ) +action.failed=Ação falhou +action.history.no_history.body=Não existe histórico disponível para {} +action.history.no_history.title=Sem histórico action.info.tooltip=Clique aqui para ver informações sobre este aplicativo -action.settings.tooltip=Clique aqui para abrir ações adicionais -any=qualquer -manage_window.name_filter.tooltip=Digite aqui para filtrar aplicativos pelo nome -manage_window.name_filter.placeholder=Filtrar por nome -publisher=publicador -unknown=desconhecido -welcome=bem vindo -gem_selector.title=Tipos de aplicativos -gem_selector.question=Quais tipos de aplicativos você quer encontrar por aqui ? -proceed=continuar -change=alterar -exit=sair -style=estilo -manage_window.bt_settings.tooltip=Clique aqui para exibir as configurações -downloading=Baixando -console.install_logs.path=Os registros de instalação podem ser encontrados em {} -author=autor -source=fonte -size=tamanho -category=categoria -categories=categorias -summary=resumo -manage_window.status.screenshots=Obtendo imagens de {} +action.not_allowed=Ação não permitida +action.request_reboot.title=Reinicialização de sistema +action.run.tooltip=Clique aqui para iniciar esse aplicativo action.screenshots.tooltip=Clique aqui para ver algumas fotos desse aplicativo -popup.screenshots.no_screenshot.body=Não foi possível obter as fotos de {} -games=jogos -game=jogo -videoeditor=edito de vídeo -browser=navegador -music=música -development=desenvolvimento -network=rede -networks=redes -graphics=gráficos -system=sistema -utility=utilidade -terminalemulator=terminal -database=banco de dados -databases=bancos de dados -text editor=editor de texto -movies=filmes -movie=filme -media=mídia -library=biblioteca -books=livros -applications=aplicativos -social=social -productivity=produtividade -utilities=utilidades -photo=foto -video=vídeo -entertainment=entretenimento -art=arte -design=desenho -reference=referência -personalisation=personalização -education=educação -health=saúde -fitness=fitness -science=ciências -news=notícias -weather=tempo -finance=finanças -desktopsettings=configurações -settings=configurações -texteditor=editor de texto -server=servidor -cloud=nuvem -instantmessaging=mensagem -2dgraphics=gŕaficos 2d -vectorgraphics=gráficos vetoriais -office=escritório -devices=dispositivos -security=segurança -audiovideo=áudio / vídeo -audio=áudio -amusement=diversão -webdevelopment=desenvolvimento web -filetransfer=transferência de arquivos -viewer=visualizador -packagemanager=gerenciador de pacotes -sportsgame=jogo de esportes -arcadegame=jogo arcade -player=jogador -fonts=fontes -screenshots.bt_next.label=próxima -screenshots.bt_back.label=anterior -screenshots,download.running=baixando imagem -screenshots.download.no_content=Sem conteúdo para exibir -screenshots.download.no_response=Imagem não encontrada -continue=continuar -stable=estável -close=fechar -publisher.verified=verificado -mirror=espelho -emulator=emulador -do_not.install=não instalar -repository=repositório -details=detalhes -communication=comunicação -messaging=mensagem -administration=administração +action.settings.tooltip=Clique aqui para abrir ações adicionais +action.update.cannot_update_label=Não podem ser atualizados +action.update.install_req.fail.body=Não foi possível instalar {} +action.update.install_req.fail.title=Instalação falhou +action.update.label_to_remove=Precisam ser desinstalados +action.update.label_to_upgrade=Serão atualizados +action.update.pkg.required_size=Requer +action.update.required_label=Precisam ser instalados +action.update.required_size=Espaço necessário em disco (Download) +action.update.requirements.body=As seguintes {} dependências precisam ser instaladas antes de atualizar +action.update.requirements.status=Verificando requisitos +action.update.requirements.title=Requisitos de atualização +action.update.status.checking_sizes=Calculando o tamanho das atualizações +action.update.success.reboot.line1=Atualizações aplicadas com sucesso ! +action.update.success.reboot.line2=Algumas alterações podem exigir a reinicialização do sistem para que surtam efeito. +action.update.success.reboot.line3=Reiniciar agora ? +action.update.summary=Resumo de atualização +action.update.total_size=Tamanho da atualização +action.trim_disk.ask=Otimizar disco (trim) ? address=endereço -view.components.file_chooser.placeholder=Clique aqui para selecionar -files=arquivos all_files=todos os arquivos -file_chooser.title=Seletor arquivos -message.file.not_exist=Arquivo não existe -message.file.not_exist.body=O arquivo {} parece não existir -icon_button.tooltip.disabled=Esta ação está indisponível -user=usuário -example.short=ex +amount=quantidade +and=e +any=qualquer +app.name=nome do aplicativo +applications=aplicativos +ask.continue=Continuar ? +ask=perguntar +author=autor +back=voltar +bt.app_not_upgrade=Não atualizar +bt.app_upgrade=Atualizar +bt.not_now=Agora não +cancel=cancelar +categories=categorias +category.2dgraphics=gŕaficos 2d +category.administration=administração +category.amusement=diversão +category.arcadegame=jogo arcade +category.art=arte +category.audio=áudio +category.audiovideo=áudio / vídeo +category.books=livros +category.browser=navegador +category.cloud=nuvem +category.communication=comunicação +category.database=banco de dados +category.databases=bancos de dados +category.design=desenho +category.desktopsettings=configurações +category.development=desenvolvimento +category.education=educação +category.emulator=emulador +category.entertainment=entretenimento +category.filetransfer=transferência de arquivos +category.finance=finanças +category.fitness=fitness +category.fonts=fontes +category.game=jogo +category.games=jogos +category.graphics=gráficos +category.health=saúde +category.instantmessaging=mensagem +category.library=biblioteca +category.media=mídia +category.messaging=mensagem +category.movie=filme +category.movies=filmes +category.music=música +category.network=rede +category.networks=redes +category.news=notícias +category.none=Nenhuma +category.office=escritório +category.packagemanager=gerenciador de pacotes +category.personalisation=personalização +category.photo=foto +category.player=jogador +category.productivity=produtividade +category.reference=referência +category.science=ciências +category.security=segurança +category.server=servidor +category.settings=configuração +category.social=social +category.sportsgame=jogo de esportes +category.system=sistema +category.terminalemulator=terminal +category.text editor=editor de texto +category.texteditor=editor de texto +category.utilities=utilidades +category.utility=utilidade +category.vectorgraphics=gráficos vetoriais +category.video=vídeo +category.videoeditor=edito de vídeo +category.viewer=visualizador +category.weather=tempo +category.web=web +category.webdevelopment=desenvolvimento web +category=categoria +change=alterar +clean=limpar +close=fechar +confirmation=confirmação +console.install_logs.path=Os registros de instalação podem ser encontrados em {} +continue=continuar +copy=copiar +core.config.backup.downgrade=Antes de reverter versão +core.config.backup.install=Antes de instalar +core.config.backup.mode.incremental.tip=Uma nova cópia de segurança do sistema será gerada contendo somente os arquivos que foram alterados a partir da última cópia existente +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.only_one.tip=Somente uma cópia de segurança do sistema será mantida. Pré-existentes serão apagadas +core.config.backup.mode.only_one=Única +core.config.backup.mode=Modo +core.config.backup.uninstall=Antes de desinstalar +core.config.backup.upgrade=Antes de atualizar +core.config.backup=Habilitada +core.config.download.icons.tip=Se os ícones dos aplicativos devem ser baixados e exibidos na tabela +core.config.download.icons=Baixar ícones +core.config.download.multithreaded.tip=Se os aplicativos, pacotes e arquivos devem ser baixados através de uma ferramenta que trabalha com segmentação / threads ( mais rápido ). No momento esta propriedade somente funcionará se o pacote aria2 estiver instalado. +core.config.download.multithreaded=Download segmentado +core.config.downloads=downloads +core.config.locale.label=idioma +core.config.mem_cache.data_exp.tip=Define o tempo de vida dos dados em memória ( em SEGUNDOS ) +core.config.mem_cache.data_exp=Expiração dos dados +core.config.mem_cache.icon_exp.tip=Define o tempo de vida dos ícones em memória ( em SEGUNDOS ) +core.config.mem_cache.icon_exp=Expiração de ícones +core.config.mem_cache=Armazenamento em memória +core.config.store_password.tip=Solicita a senha do usuário root somente uma vez +core.config.store_password=Solicitação única de senha +core.config.suggestions.activated.tip=Se novos aplicativos podem ser sugeridos para instalação +core.config.suggestions.activated=Sugestões +core.config.suggestions.by_type.tip=Número máximo de sugestões que devem ser exibidas por tipo de aplicativo +core.config.suggestions.by_type=Sugestões por tipo +core.config.system.dep_checking.tip=Se a verificação da disponibilidade das tecnologias do seu sistema devem ocorrer somente uma vez +core.config.system.dep_checking=Verificação única de sistema +core.config.system.notifications.tip=Se notificações devem ser exibidas quando uma ação é finalizada ou existem atualizações +core.config.system.notifications=Notificações core.config.tab.advanced=Avançadas +core.config.tab.backup=Cópia de Segurança core.config.tab.general=Gerais -core.config.tab.ui=Interface core.config.tab.tray=Bandeja core.config.tab.types=Tipos -core.config.locale.label=idioma -core.config.disk_cache=Cache em disco -core.config.disk_cache.tip=Alguns dados sobre os aplicativos instalados serão armazenados em disco para que os mesmos sejam carregados rapidamente nas próximas inicializações -core.config.updates.interval=Intervalo de atualizações -core.config.updates.interval.tip=Define o intervalo de tempo em que ocorrerá a verificação de atualizações para os aplicativos instalados ( em SEGUNDOS ) -core.config.downloads=downloads -core.config.download.icons=Baixar ícones -core.config.download.icons.tip=Se os ícones dos aplicativos devem ser baixados e exibidos na tabela -core.config.download.multithreaded=Download segmentado -core.config.download.multithreaded.tip=Se os aplicativos, pacotes e arquivos devem ser baixados através de uma ferramenta que trabalha com segmentação / threads ( mais rápido ). No momento esta propriedade somente funcionará se o pacote aria2 estiver instalado. -core.config.mem_cache=Armazenamento em memória -core.config.mem_cache.data_exp=Expiração dos dados -core.config.mem_cache.data_exp.tip=Define o tempo de vida dos dados em memória ( em SEGUNDOS ) -core.config.mem_cache.icon_exp=Expiração de ícones -core.config.mem_cache.icon_exp.tip=Define o tempo de vida dos ícones em memória ( em SEGUNDOS ) -core.config.suggestions.activated=Sugestões -core.config.suggestions.activated.tip=Se novos aplicativos podem ser sugeridos para instalação -core.config.system.notifications=Notificações -core.config.system.notifications.tip=Se notificações devem ser exibidas quando uma ação é finalizada ou existem atualizações -core.config.ui.hdpi=HDPI -core.config.ui.hdpi.tip=Se melhorias para resoluções HDPI devem ser ativadas -core.config.ui.max_displayed=Aplicativos exibidos -core.config.ui.max_displayed.tip=Número máximo de aplicativos exibidos na tabela -core.config.ui.tray.default_icon=Ícone padrão -core.config.ui.tray.default_icon.tip=O ícone padrão exibido na bandeja -core.config.ui.tray.updates_icon=Ícone de atualização -core.config.ui.tray.updates_icon.tip=O ícone exibido quando há atualizações disponíveis -core.config.system.dep_checking=Verificação única de sistema -core.config.system.dep_checking.tip=Se a verificação da disponibilidade das tecnologias do seu sistema devem ocorrer somente uma vez -core.config.suggestions.by_type=Sugestões por tipo -core.config.suggestions.by_type.tip=Número máximo de sugestões que devem ser exibidas por tipo de aplicativo +core.config.tab.ui=Interface +core.config.trim_after_update.tip=Otimiza o disco após uma atualização bem sucedida utilizando "trim". Não habilite essa opção se o seu disco não for um SSD que permite trim. +core.config.trim_after_update=Otimizar disco após atualizar core.config.types.tip=Marque os tipos de aplicativo que você quer gerenciar -core.config.ui.auto_scale=Escala automática core.config.ui.auto_scale.tip=Ativa o fator de escala automático ( {} ). Corrige problemas de escala para alguns ambientes desktop. -core.config.updates.sort_pkgs=Organizar atualizações -core.config.updates.sort_pkgs.tip=Define a melhor ordem de atualização para os aplicativos / pacotes selecionados para evitar problemas -core.config.updates.dep_check=Mostrar requisitos de atualização -core.config.updates.dep_check.tip=Exibe todos as dependências que precisam ser instaladas antes de iniciar a atualização dos selecionados -settings.changed.success.warning=Configurações alteradas com sucesso ! Algumas delas só surtirão após a reinicialização. -settings.changed.success.reboot=Reiniciar agora ? -settings.error=Não foi possível alterar todas as configurações adequadamente -locale.en=inglês -locale.es=espanhol -locale.pt=português +core.config.ui.auto_scale=Escala automática +core.config.ui.hdpi.tip=Se melhorias para resoluções HDPI devem ser ativadas +core.config.ui.hdpi=HDPI +core.config.ui.max_displayed.tip=Número máximo de aplicativos exibidos na tabela +core.config.ui.max_displayed=Aplicativos exibidos +core.config.ui.tray.default_icon.tip=O ícone padrão exibido na bandeja +core.config.ui.tray.default_icon=Ícone padrão +core.config.ui.tray.updates_icon.tip=O ícone exibido quando há atualizações disponíveis +core.config.ui.tray.updates_icon=Ícone de atualização +core.config.updates.interval.tip=Define o intervalo de tempo em que ocorrerá a verificação de atualizações para os aplicativos instalados ( em SEGUNDOS ) +core.config.updates.interval=Intervalo de atualizações +custom_action.proceed_with=Continuar com {} ? +description=descrição +details=detalhes +devices=dispositivos +do_not.install=não instalar +downgraded=versão revertida +download=download +downloading=Baixando +error=erro +example.short=ex +exit=sair +file=arquivo +file_chooser.title=Seletor arquivos +files=arquivos +finishing=finalizando +gem_selector.question=Quais tipos de aplicativos você quer encontrar por aqui ? +gem_selector.title=Tipos de aplicativos +general=geral +icon_button.tooltip.disabled=Esta ação está indisponível +imported=importado +initialization=inicialização +install=instalar +installation=instalação +installed=instalado +internet.required=É necessário estar conectado a Internet +interval=intervalo +latest_version=última versão +license=licença locale.ca=catalão locale.de=alemão +locale.en=inglês +locale.es=espanhol locale.it=italiano +locale.pt=português locale.ru=russo -interval=intervalo -installation=instalação -download=download -clean=limpar -action.update.status.sorting=Determinando a melhor ordem de atualização -action.update.requirements.title=Requisitos de atualização -action.update.requirements.body=As seguintes {} dependências precisam ser instaladas antes de atualizar -action.update.install_req.fail.title=Instalação falhou -action.update.install_req.fail.body=Não foi possível instalar {} -action.update.requirements.status=Verificando requisitos -action.update.order.title=Ordem de atualização -action.update.order.body=A atualização será realizada na seguinte ordem \ No newline at end of file +manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ? +manage_window.apps_table.row.actions.downgrade=Reverter versão +manage_window.apps_table.row.actions.history=Histórico +manage_window.apps_table.row.actions.info=Informação +manage_window.apps_table.row.actions.install.popup.body=Instalar {} no seu dispositivo ? +manage_window.apps_table.row.actions.install.popup.title=Instalação +manage_window.apps_table.row.actions.install=Instalar +manage_window.apps_table.row.actions.refresh=Atualizar +manage_window.apps_table.row.actions.uninstall.popup.body=Remover {} do seu dispositivo ? +manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação +manage_window.apps_table.row.actions.uninstall=Desinstalar +manage_window.apps_table.upgrade_toggle.disabled.tooltip=Porém não é possível atualiza-lo. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Clique aqui para marcar ou desmarcar a atualização. +manage_window.apps_table.upgrade_toggle.tooltip=Existe um atualização para esse aplicativo. +manage_window.bt.installed.text=instalados +manage_window.bt.installed.tooltip=Clique aqui para exibir os aplicativos instalados +manage_window.bt.refresh.text=Recarregar +manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados +manage_window.bt.suggestions.text=Sugestões +manage_window.bt.suggestions.tooltip=Clique aqui para exibir algumas sugestões de aplicativos +manage_window.bt.upgrade.text=Atualizar +manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos marcados +manage_window.bt_custom_actions.tip=Clique aqui para exibir mais ações +manage_window.bt_settings.tooltip=Clique aqui para exibir as configurações +manage_window.checkbox.only_apps=Aplicativos +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.label.updates=Atualizações +manage_window.name_filter.placeholder=Filtrar por nome +manage_window.name_filter.tooltip=Digite aqui para filtrar aplicativos pelo nome +manage_window.settings.about=Sobre +manage_window.status.downgrading=Revertendo +manage_window.status.filtering=Filtrando +manage_window.status.history=Obtendo histórico +manage_window.status.info=Obtendo informação +manage_window.status.installed=Carregando instalados +manage_window.status.installing=Instalando +manage_window.status.refreshing=Recarregando +manage_window.status.refreshing_app=Atualizando +manage_window.status.running_app=Iniciando {} +manage_window.status.screenshots=Obtendo imagens de {} +manage_window.status.searching=Buscando +manage_window.status.suggestions=Procurando sugestões +manage_window.status.uninstalling=Desinstalando +manage_window.status.upgrading=Atualizando +manage_window.title=meus aplicativos +manage_window.upgrade_all.popup.body=Atualizar todos os aplicativos selecionados ? +manage_window.upgrade_all.popup.title=Atualizar +message.file.not_exist.body=O arquivo {} parece não existir +message.file.not_exist=Arquivo não existe +mirror=espelho +name=nome +no=não +not_installed=não instalado +notification.downgrade.failed=Erro ao reverter versão +notification.install.failed=instalação falhou +notification.uninstall.failed=desinstalação falhou +notification.update_selected.failed=Erro ao atualizar +notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso +ok=ok +others=outros +popup.button.cancel=Cancelar +popup.button.no=Não +popup.button.yes=Sim +popup.history.selected.tooltip=Versão atual +popup.history.title=Histórico +popup.root.bad_password.body=Senha incorreta +popup.root.bad_password.last_try=Tentativas finalizadas. Ação cancelada. +popup.root.bad_password.title=Erro +popup.root.continue=prosseguir +popup.root.title=Requer sua senha para prosseguir +popup.screenshots.no_screenshot.body=Não foi possível obter as fotos de {} +prepare_panel.bt_skip.label=Pular +prepare_panel.title.finish=Pronto +prepare_panel.title.start=Iniciando +proceed=continuar +publisher.verified=verificado +publisher=publicador +repository=repositório +screenshots.download.running=baixando imagem +screenshots.bt_back.label=anterior +screenshots.bt_next.label=próxima +screenshots.download.no_content=Sem conteúdo para exibir +screenshots.download.no_response=Imagem não encontrada +settings.changed.success.reboot=Reiniciar agora ? +settings.changed.success.warning=Configurações alteradas com sucesso ! Algumas delas só surtirão após a reinicialização. +settings.error=Não foi possível alterar todas as configurações adequadamente +settings=configurações +show=mostrar +size=tamanho +source=fonte +stable=estável +status.caching_data=Cacheando dados de {} para o disco +style=estilo +success=sucesso +summary=resumo +task.download_categories=Baixando categorias [ {} ] +type=tipo +uninstall=desinstalar +uninstalled=desinstalado +unknown=desconhecido +updates=atualizações +user=usuário +version.installed=versão instalada +version.installed_outdated=a versão instalada está desatualizada +version.latest=versão mais recente +version.outdated=desatualizada +version.unknown=versão não informada +version.updated=atualizada +version=versão +view.components.file_chooser.placeholder=Clique aqui para selecionar +warning=aviso +warning.update_available=Há uma nova atualização do {} disponível ({}). Confira as novidades em {} +welcome=bem vindo +window_manage.input_search.placeholder=Buscar +window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos +yes=sim \ No newline at end of file diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 7dbb08f9..cf580b18 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -1,205 +1,216 @@ -manage_window.title=Мои приложения -manage_window.columns.latest_version=Последняя версия -manage_window.columns.update=Обновить ? -manage_window.apps_table.row.actions.info=Информация -manage_window.apps_table.row.actions.history=История версий -manage_window.apps_table.row.actions.uninstall.popup.title=Удаление -manage_window.apps_table.row.actions.uninstall.popup.body=Удалить {} с Вашего устройства ? -manage_window.apps_table.row.actions.install.popup.title=Установка -manage_window.apps_table.row.actions.install.popup.body=Установить {} на Ваше устройство ? -manage_window.apps_table.row.actions.downgrade=Откат версии -manage_window.apps_table.row.actions.downgrade.popup.body=Вы действительно хотите откатить версию {} ? -manage_window.apps_table.row.actions.install=Установка -manage_window.apps_table.row.actions.refresh=Обновить список -manage_window.apps_table.upgrade_toggle.tooltip=Есть обновление для этого приложения. Нажмите здесь, чтобы поставить или снять флажок обновления -manage_window.checkbox.only_apps=Приложения -window_manage.input_search.placeholder=Поиск -window_manage.input_search.tooltip=Введите и нажмите ENTER для поиска приложений -manage_window.label.updates=Обновления -manage_window.status.refreshing=Обновляется -manage_window.status.upgrading=Обновление -manage_window.status.uninstalling=Удалить -manage_window.status.downgrading=Откатить -manage_window.status.info=Извлечение информации -manage_window.status.history=Извлечение истории -manage_window.status.searching=Поиск -manage_window.status.installing=Установить -manage_window.status.refreshing_app=Обновляется -manage_window.status.running_app=Запускается {} -manage_window.status.filtering=Фильтрация -manage_window.status.screenshots=Получение скриншотов {} -manage_window.status.suggestions=Поиск рекомендаций -manage_window.bt.refresh.text=Обновить список -manage_window.bt.refresh.tooltip=Перезагрузите данные об установленных приложениях -manage_window.bt.upgrade.text=Обновление -manage_window.bt.upgrade.tooltip=Обновить все выбранные приложения -manage_window.bt.installed.text=Установленные -manage_window.bt.installed.tooltip=Нажмите здесь, чтобы показать установленные приложения -manage_window.bt.suggestions.text=Рекомендации -manage_window.bt.suggestions.tooltip=Нажмите здесь, чтобы показать некоторые рекомендаваные приложения -manage_window.checkbox.show_details=Показать детали -popup.root.title=Требуется ваш пароль, чтобы продолжить -popup.root.continue=Продолжить -popup.root.bad_password.title=Ошибка -popup.root.bad_password.body=Неправильный пароль -popup.root.bad_password.last_try=Закончились попытки. Отмена действия. -popup.history.title=История версий -popup.history.selected.tooltip=Текущая версия -popup.button.yes=Да -popup.button.no=Нет -popup.button.cancel=Отмена -tray.action.manage=Приложения -tray.action.exit=Выход -tray.action.about=О программе -tray.action.refreshing=Обновляется +Australia=Австралия +Austria=Австрия +Bangladesh=Бангладеш +Belarus=Беларусь +Belgium=Бельгия +Brazil=Бразилия +Bulgaria=Болгария +Canada=Канада +Chile=Чили +China=Китай +Costa Rica=Коста-Рика +Czech=Чехия +Denmark=Дания +Ecuador=Эквадор +France=Франция +Georgia=Грузия +Germany=Германия +Greece=Греция +Hong Kong=Гонконг +Hungary=Венгрия +Iceland=Исландия +India=Индия +Indonesia=Индонезия +Iran=Иран +Italy=Италия +Japan=Япония +Kenya=Кения +Netherlands=Нидерланды +New Zealand=Новая Зеландия +Norway=Норвегия +Philippines=Филиппины +Poland=Польша +Portugal=Португалия +Russia=Российская Федерация +Singapore=Сингапур +South Africa=Южно-Африканская Республика +South Korea=Южная Корея +Spain=Испания +Sweden=Швеция +Switzerland=Швейцария +Taiwan=Тайвань +Thailand=Тайланд +Turkey=Турция +Ukraine=Украина +United Kingdom=Великобритания +United States=Соединённые Штаты Америки +action.backup.error.create=It was not possible to generate a new system copy +action.backup.error.delete=It was not possible to delete the old system copies +action.backup.error.proceed=Proceed anyway ? +action.backup.invalid_mode=Invalid backup mode +action.backup.msg=Do you want to generate a system copy before proceeding ? +action.backup.substatus.create=Generating a new system backup +action.backup.substatus.delete=Removing old system backups +action.cancelled=Операция отменена пользователем +action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim.error=There was a problem while optimizing the disk +action.failed=Действие не удалось +action.history.no_history.body=There is no available history for {} +action.history.no_history.title=No history action.info.tooltip=Нажмите здесь, чтобы посмотреть информацию об этом приложении +action.not_allowed=Действие не допускается +action.request_reboot.title=System restart +action.run.tooltip=Нажмите здесь, чтобы запустить это приложение action.screenshots.tooltip=Нажмите здесь, чтобы увидеть некоторые скриншоты этого приложения action.settings.tooltip=Нажмите здесь, чтобы открыть дополнительные действия -notification.update={} обновления доступно -notification.updates={} обновлений доступно -notification.update_selected.success=Приложение(я) успешно обновлено(ы) -notification.update_selected.failed=Обновление не удалось -notification.install.failed=Установка не удалась -notification.uninstall.failed=Удаление не удалось -notification.downgrade.failed=Откат не удался -about.info.desc=Графический интерфейс для управления приложениями Linux -about.info.link=Ещё информация на -about.info.license=Свободная лицензия -about.info.rate.question=Понравилось это приложеие ? -about.info.rate.answer=Поставьте ему звезду на GitHub, чтобы поддержать проект -about.info.trouble.question=Возникли проблемы ? -about.info.trouble.answer=Откройте GitHub, чтобы разработчики могли вам помочь -yes=Да -no=Нет -version.updated=Обновленная -version.outdated=Устаревшая -name=Имя -version=Версия -latest_version=Последняя версия -description=Описание -type=Тип -installed=Установлено -uninstalled=Деинсталлировано -not_installed=Не установлено -downgraded=Произведён откат -others=Другие -internet.required=Требуется подключение к интернету -updates=Обновления -version.installed=Установленная версия -version.latest=Последняя версия -version.installed_outdated=Установленная версия устарела -version.unknown=не сообщать -app.name=Название приложения -warning=Предупреждение -install=Установка -uninstall=Деинсталляция -bt.app_upgrade=Обновить -bt.app_not_upgrade=Не обновлять -manage_window.upgrade_all.popup.title=Обновление -manage_window.upgrade_all.popup.body=Обновить все выбранные приложения ? -manage_window.settings.about=О программе -confirmation=Подтверждение -and=и -error=Ошибка -action.cancelled=Операция отменена пользователем -copy=Копировать -back=Назад -show=Показать -ask.continue=Продолжить ? -cancel=Отмена -status.caching_data=Кэширование данных {} на диск -action.run.tooltip=Нажмите здесь, чтобы запустить это приложение -any=несколько -manage_window.name_filter.tooltip=Введите здесь, чтобы фильтровать приложения по имени -manage_window.name_filter.placeholder=Фильтр по имени -publisher=издатель -unknown=неизвестный -welcome=Добро пожаловать! -gem_selector.title=Типы приложений -gem_selector.question=Какие типы приложений вы хотите здесь найти ? -proceed=Продолжить -change=Изменить -exit=Выход -style=Стиль -manage_window.bt_settings.tooltip=Нажмите здесь, чтобы отобразить настройки -downloading=Загрузка -console.install_logs.path=Журналы установки можно найти по адресу {} -author=Автор -source=Источник -size=Размер -category=Категория -categories=Категории -summary=Суммарно -popup.screenshots.no_screenshot.body=не удалось получить скриншоты {} -terminalemulator=Эмулятор терминала -desktopsettings=Настройки рабочего стола -texteditor=Текстовый редактор -screenshots.bt_next.label=Следующий -screenshots.bt_back.label=Предыдущий -instantmessaging=Мессенджер -2dgraphics=2D-графика -vectorgraphics=Векторная графика -audiovideo=Аудио/Видео -webdevelopment=Web-разработка -filetransfer=Передача файлов -packagemanager=Пакетный менеджер -sportsgame=Спортивная игра -arcadegame=Аркада -player=Проигрыватель -screenshots,download.running=Загрузка изображения -screenshots.download.no_content=Нет материалов для отображени -screenshots.download.no_response=Изображение не найдено -continue=Продолжить -stable=Стабильная -close=Закрыть -publisher.verified=Проверенный -mirror=Зеркало -emulator=Эмуляторы -do_not.install=не устанавливать -repository=Репозиторий -details=Детали -communication=Общение -web=Сеть -office=Офис -messaging=Сообщения -administration=Администрирование -audio=Аудио -video=Видео -settings=Настройки +action.update.cannot_update_label=Cannot be upgraded +action.update.install_req.fail.body=Невозможно установить {} +action.update.install_req.fail.title=Установка не удалась +action.update.label_to_remove=Must be uninstalled +action.update.label_to_upgrade=Will be upgraded +action.update.pkg.required_size=Requires +action.update.required_label=Должен быть установлен +action.update.required_size=Disk space required (download) +action.update.requirements.body=Перед обновлением необходимо установить следующие зависимости {} +action.update.requirements.status=Проверка требований +action.update.requirements.title=Требования к обновлению +action.update.status.checking_sizes=Расчет размера обновления +action.update.success.reboot.line1=Upgrades successfully applied ! +action.update.success.reboot.line2=Some changes may require a system restart to take effect. +action.update.success.reboot.line3=Restart now ? +action.update.summary=Сводка обновления +action.update.total_size=Update size +action.trim_disk.ask=Optimize disc (trim) ? address=Адрес -view.components.file_chooser.placeholder=Нажмите здесь, чтобы выбрать -files=Файлы all_files=Все файлы -file_chooser.title=Выбор файлов -message.file.not_exist=Файл не существует -message.file.not_exist.body=Файл {}, кажется, не существует -development=Разработка -icon_button.tooltip.disabled=Это действие недоступно -user=Пользователь -example.short=к примеру -core.config.tab.advanced=Продвинутые -core.config.tab.general=Основные -core.config.tab.ui=Интерфейс -core.config.tab.tray=Трей -core.config.tab.types=Типы -core.config.locale.label=Язык -core.config.disk_cache=Дисковый кэш -core.config.disk_cache.tip=Некоторые данные об установленных приложениях будут сохранены на диске, так что они могут быть быстро загружены в следующих инициализациях -core.config.updates.interval=Интервал обновления -core.config.updates.interval.tip=Определяет интервал времени, в течение которого будет происходить проверка обновлений для установленных приложений ( в секундах ) +amount=amount +and=и +any=несколько +app.name=Название приложения +applications=applications +ask=Спросить +ask.continue=Продолжить ? +author=Автор +back=Назад +bt.app_not_upgrade=Не обновлять +bt.app_upgrade=Обновить +bt.not_now=Не сейчас +cancel=Отмена +categories=Категории +category=Категория +category.2dgraphics=2D-графика +category.administration=Администрирование +category.amusement=amusement +category.arcadegame=Аркада +category.art=art +category.audio=Аудио +category.audiovideo=Аудио/Видео +category.books=books +category.browser=browser +category.cloud=cloud +category.communication=Общение +category.database=database +category.databases=databases +category.design=design +category.desktopsettings=Настройки рабочего стола +category.development=Разработка +category.education=education +category.emulator=Эмуляторы +category.entertainment=entertainment +category.filetransfer=Передача файлов +category.finance=finance +category.fitness=fitness +category.fonts=fonts +category.game=game +category.games=games +category.graphics=graphics +category.health=health +category.instantmessaging=Мессенджер +category.library=library +category.media=media +category.messaging=Сообщения +category.movie=movie +category.movies=movies +category.music=music +category.network=network +category.networks=networks +category.news=news +category.none=нет +category.office=Офис +category.packagemanager=Пакетный менеджер +category.personalisation=personalization +category.photo=photo +category.player=Проигрыватель +category.productivity=productivity +category.reference=reference +category.science=science +category.security=security +category.server=server +category.settings=Настройки +category.social=social +category.sportsgame=Спортивная игра +category.system=system +category.terminalemulator=Эмулятор терминала +category.text editor=text editor +category.texteditor=Текстовый редактор +category.utilities=utilities +category.utility=utility +category.vectorgraphics=Векторная графика +category.video=Видео +category.videoeditor=video editor +category.viewer=viewer +category.weather=weather +category.web=Сеть +category.webdevelopment=Web-разработка +change=Изменить +clean=Очистка +close=Закрыть +confirmation=Подтверждение +console.install_logs.path=Журналы установки можно найти по адресу {} +continue=Продолжить +copy=Копировать +core.config.backup=Enabled +core.config.backup.downgrade=Before downgrading +core.config.backup.install=Before installing +core.config.backup.mode=Mode +core.config.backup.mode.incremental=Incremental +core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy. +core.config.backup.mode.only_one=Single +core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased. +core.config.backup.uninstall=Before uninstalling +core.config.backup.upgrade=Before upgrading core.config.download.icons=Скачать иконки core.config.download.icons.tip=Загружать иконки приложения для отображаения на рабочем столе core.config.download.multithreaded=Многопоточная загрузка core.config.download.multithreaded.tip=Следует ли загружать приложения, пакеты и файлы с помощью инструмента, который работает с потоками ( быстрее ). На данный момент эта настройка будет работать только в том случае, если установлен пакет aria2 +core.config.downloads=downloads +core.config.locale.label=Язык core.config.mem_cache=Запоминающее устройство core.config.mem_cache.data_exp=Истечение срока действия данных core.config.mem_cache.data_exp.tip=Определяет время жизни данных в памяти ( в секундах ) core.config.mem_cache.icon_exp=Истечение срока действия иконок core.config.mem_cache.icon_exp.tip=Определяет время жизни значков в памяти (в секундах ) +core.config.store_password=Однократный запрос пароля +core.config.store_password.tip=Запрашивать пароль пользователя root только один раз core.config.suggestions.activated=Рекомендации core.config.suggestions.activated.tip=Отображать рекомендумые к установке приложения. +core.config.suggestions.by_type=Рекомендации по типу +core.config.suggestions.by_type.tip=Максимальное количество рекомендаций, которые должны отображаться по типу приложения +core.config.system.dep_checking=Одкократная проверка системы. +core.config.system.dep_checking.tip=Если проверка доступности ваших системных техноллгий должна происходить только один раз core.config.system.notifications=Уведомления core.config.system.notifications.tip=Отображать уведомления о завершении операции или об имеющихся обновлниях +core.config.tab.advanced=Продвинутые +core.config.tab.backup=Backup +core.config.tab.general=Основные +core.config.tab.tray=Трей +core.config.tab.types=Типы +core.config.tab.ui=Интерфейс +core.config.trim_after_update=Optimize disc after upgrading +core.config.trim_after_update.tip=It optimizes the disc after a successfull upgrade using "trim". Do not enable this option if your disk is not a SSD that allows trim. +core.config.types.tip=Отметьте типы приложений, которыми вы хотите управлять +core.config.ui.auto_scale=Автоматическое масштабирование +core.config.ui.auto_scale.tip=Он активирует автоматический коэффициент масштабирования экрана ({}),тем самым устраняет проблемы масштабирования для некоторых сред рабочего стола core.config.ui.hdpi=HDPI core.config.ui.hdpi.tip=Если улучшения, связанные с разрешением HDPI должны быть активированы core.config.ui.max_displayed=Отображаемые приложения @@ -208,36 +219,159 @@ core.config.ui.tray.default_icon=Значок по умолчанию core.config.ui.tray.default_icon.tip=Значок по умолчанию для {app}, отображаемый в трее core.config.ui.tray.updates_icon=Значок обновления core.config.ui.tray.updates_icon.tip=Значок, отображаемый при наличии доступных обновлений -core.config.system.dep_checking=Одкократная проверка системы. -core.config.system.dep_checking.tip=Если проверка доступности ваших системных техноллгий должна происходить только один раз -core.config.suggestions.by_type=Рекомендации по типу -core.config.suggestions.by_type.tip=Максимальное количество рекомендаций, которые должны отображаться по типу приложения -core.config.types.tip=Отметьте типы приложений, которыми вы хотите управлять -core.config.ui.auto_scale=Автоматическое масштабирование -core.config.ui.auto_scale.tip=Он активирует автоматический коэффициент масштабирования экрана ({}),тем самым устраняет проблемы масштабирования для некоторых сред рабочего стола -core.config.updates.sort_pkgs=Обновление сортировки -core.config.updates.sort_pkgs.tip=Определяет оптимальный порядок обновления для выбранных приложений / пакетов, чтобы избежать проблем -core.config.updates.dep_check=Показать требования к обновлению -core.config.updates.dep_check.tip=Отображает все зависимости, которые необходимо установить перед началом обновления выбранных -settings.changed.success.warning=Настройки успешно изменены. Некоторые из них вступят в силу только после перезагрузки. -settings.changed.success.reboot=Перезапустить сейчас ? -settings.error=Невозможно было правильно изменить все настройки -locale.en=Английский -locale.es=Испанский -locale.pt=Поргтугальский +core.config.updates.interval=Интервал обновления +core.config.updates.interval.tip=Определяет интервал времени, в течение которого будет происходить проверка обновлений для установленных приложений ( в секундах ) +custom_action.proceed_with=Proceed with {} ? +description=Описание +details=Детали +devices=devices +do_not.install=не устанавливать +downgraded=Произведён откат +download=Загрузка +downloading=Загрузка +error=Ошибка +example.short=К примеру +exit=Выход +file=файл +file_chooser.title=Выбор файлов +files=Файлы +finishing=finishing +gem_selector.question=Какие типы приложений вы хотите здесь найти ? +gem_selector.title=Типы приложений +general=general +icon_button.tooltip.disabled=Это действие недоступно +imported=импортировано +initialization=инициализация +install=Установка +installation=Установка +installed=Установлено +internet.required=Требуется подключение к интернету +interval=Интервал +latest_version=Последняя версия +license=лизензия locale.ca=Каталонский locale.de=Немецкий +locale.en=Английский +locale.es=Испанский locale.it=Итальянский +locale.pt=Поргтугальский locale.ru=Русский -interval=Интервал -installation=Установка -download=Загрузка -clean=Очистка -action.update.status.sorting=Определение наилучшего порядка обновления -action.update.requirements.title=Требования к обновлению -action.update.requirements.body=Перед обновлением необходимо установить следующие зависимости {} -action.update.install_req.fail.title=Установка не удалась -action.update.install_req.fail.body=Невозможно установить {} -action.update.requirements.status=Проверка требований -action.update.order.title=Порядок обновления -action.update.order.body=Обновление будет выполнено в следующем порядке +manage_window.apps_table.row.actions.downgrade=Откат версии +manage_window.apps_table.row.actions.downgrade.popup.body=Вы действительно хотите откатить версию {}? +manage_window.apps_table.row.actions.history=История версий +manage_window.apps_table.row.actions.info=Информация +manage_window.apps_table.row.actions.install=Установка +manage_window.apps_table.row.actions.install.popup.body=Установить {} на Ваше устройство? +manage_window.apps_table.row.actions.install.popup.title=Установка +manage_window.apps_table.row.actions.refresh=Обновить список +manage_window.apps_table.row.actions.uninstall=Uninstall +manage_window.apps_table.row.actions.uninstall.popup.body=Удалить {} с Вашего устройства? +manage_window.apps_table.row.actions.uninstall.popup.title=Удаление +manage_window.apps_table.upgrade_toggle.disabled.tooltip=Но обновить его невозможно. +manage_window.apps_table.upgrade_toggle.enabled.tooltip=Нажмите здесь, чтобы поставить или снять флажок обновления. +manage_window.apps_table.upgrade_toggle.tooltip=Есть обновление для этого приложения. +manage_window.bt.installed.text=Установленные +manage_window.bt.installed.tooltip=Нажмите здесь, чтобы показать установленные приложения +manage_window.bt.refresh.text=Обновить список +manage_window.bt.refresh.tooltip=Перезагрузите данные об установленных приложениях +manage_window.bt.suggestions.text=Рекомендации +manage_window.bt.suggestions.tooltip=Нажмите здесь, чтобы показать некоторые рекомендаваные приложения +manage_window.bt.upgrade.text=Обновление +manage_window.bt.upgrade.tooltip=Обновить все выбранные приложения +manage_window.bt_custom_actions.tip=Нажмите здесь, чтобы показать больше действий +manage_window.bt_settings.tooltip=Нажмите здесь, чтобы отобразить настройки +manage_window.checkbox.only_apps=Приложения +manage_window.checkbox.show_details=Показать детали +manage_window.columns.installed=Installed +manage_window.columns.latest_version=Последняя версия +manage_window.columns.update=Обновить? +manage_window.label.updates=Обновления +manage_window.name_filter.placeholder=Фильтр по имени +manage_window.name_filter.tooltip=Введите здесь, чтобы фильтровать приложения по имени +manage_window.settings.about=О программе +manage_window.status.downgrading=Откатить +manage_window.status.filtering=Фильтрация +manage_window.status.history=Извлечение истории +manage_window.status.info=Извлечение информации +manage_window.status.installed=Загрузка установленных +manage_window.status.installing=Установить +manage_window.status.refreshing=Обновляется +manage_window.status.refreshing_app=Обновляется +manage_window.status.running_app=Запускается {} +manage_window.status.screenshots=Получение скриншотов {} +manage_window.status.searching=Поиск +manage_window.status.suggestions=Поиск рекомендаций +manage_window.status.uninstalling=Удалить +manage_window.status.upgrading=Обновление +manage_window.title=Мои приложения +manage_window.upgrade_all.popup.body=Обновить все выбранные приложения ? +manage_window.upgrade_all.popup.title=Обновление +message.file.not_exist=Файл не существует +message.file.not_exist.body=Файл {}, кажется, не существует +mirror=Зеркало +name=Имя +no=Нет +not_installed=Не установлено +notification.downgrade.failed=Откат не удался +notification.install.failed=Установка не удалась +notification.uninstall.failed=Удаление не удалось +notification.update_selected.failed=Обновление не удалось +notification.update_selected.success=Приложение(я) успешно обновлено(ы) +ok=ok +others=Другие +popup.button.cancel=Отмена +popup.button.no=Нет +popup.button.yes=Да +popup.history.selected.tooltip=Текущая версия +popup.history.title=История версий +popup.root.bad_password.body=Неправильный пароль +popup.root.bad_password.last_try=Закончились попытки. Отмена действия. +popup.root.bad_password.title=Ошибка +popup.root.continue=Продолжить +popup.root.title=Требуется ваш пароль, чтобы продолжить +popup.screenshots.no_screenshot.body=не удалось получить скриншоты {} +prepare_panel.bt_skip.label=Пропустить +prepare_panel.title.finish=Ready +prepare_panel.title.start=Initializing +proceed=Продолжить +publisher=издатель +publisher.verified=Проверенный +repository=Репозиторий +screenshots.bt_back.label=Предыдущий +screenshots.bt_next.label=Следующий +screenshots.download.no_content=Нет материалов для отображени +screenshots.download.no_response=Изображение не найдено +screenshots.download.running=Загрузка изображения +settings=Настройки +settings.changed.success.reboot=Перезапустить сейчас? +settings.changed.success.warning=Настройки успешно изменены. Некоторые из них вступят в силу только после перезагрузки. +settings.error=Не удалось правильно изменить все настройки +show=Показать +size=Размер +source=Источник +stable=Стабильная +status.caching_data=Кэширование данных {} на диск +style=Стиль +success=success +summary=Суммарно +task.download_categories=Downloading categories [ {} ] +type=Тип +uninstall=Деинсталляция +uninstalled=Деинсталлировано +unknown=неизвестный +updates=Обновления +user=Пользователь +version=Версия +version.installed=Установленная версия +version.installed_outdated=Установленная версия устарела +version.latest=Последняя версия +version.outdated=Устаревшая +version.unknown=не сообщать +version.updated=Обновленная +view.components.file_chooser.placeholder=Нажмите здесь, чтобы выбрать +warning=Предупреждение +warning.update_available=There is a new {} update available ({}). Checkout the news at {}. +welcome=Добро пожаловать! +window_manage.input_search.placeholder=Поиск +window_manage.input_search.tooltip=Введите и нажмите ENTER для поиска приложений +yes=Да \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/ca b/bauh/view/resources/locale/tray/ca new file mode 100644 index 00000000..7997de31 --- /dev/null +++ b/bauh/view/resources/locale/tray/ca @@ -0,0 +1,7 @@ +tray.action.manage=Aplicacions +tray.action.exit=Surt +tray.action.about=Quant a +tray.action.refreshing=Refrescant +tray.settings=configuració +notification.update={} actualització disponible +notification.updates={} actualitzacions disponibles \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/de b/bauh/view/resources/locale/tray/de new file mode 100644 index 00000000..c22e50e8 --- /dev/null +++ b/bauh/view/resources/locale/tray/de @@ -0,0 +1,7 @@ +tray.action.manage=Anwendungen +tray.action.exit=Beenden +tray.action.about=Über +tray.action.refreshing=Aktualisieren +tray.settings=Einstellungen +notification.update={} Update verfügbar +notification.updates={} Updates verfügbar \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/en b/bauh/view/resources/locale/tray/en new file mode 100644 index 00000000..217cccad --- /dev/null +++ b/bauh/view/resources/locale/tray/en @@ -0,0 +1,7 @@ +tray.action.manage=Applications +tray.action.exit=Quit +tray.action.about=About +tray.action.refreshing=Refreshing +tray.settings=Settings +notification.update={} available update +notification.updates={} available updates \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/es b/bauh/view/resources/locale/tray/es new file mode 100644 index 00000000..0d4c2301 --- /dev/null +++ b/bauh/view/resources/locale/tray/es @@ -0,0 +1,7 @@ +tray.action.manage=Aplicaciones +tray.action.exit=Cerrar +tray.action.about=Acerca de +tray.action.refreshing=Recargando +tray.settings=configuraciones +notification.update={} actualización disponible +notification.updates={} actualizaciones disponibles \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/it b/bauh/view/resources/locale/tray/it new file mode 100644 index 00000000..31d8d5cf --- /dev/null +++ b/bauh/view/resources/locale/tray/it @@ -0,0 +1,7 @@ +tray.action.manage=Applicazioni +tray.action.exit=Abbandona +tray.action.about=Informazioni su +tray.action.refreshing=Refreshing +tray.settings=impostazioni +notification.update={} aggiornamento disponibile +notification.updates={} aggiornamenti disponibile \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/pt b/bauh/view/resources/locale/tray/pt new file mode 100644 index 00000000..9983dfe6 --- /dev/null +++ b/bauh/view/resources/locale/tray/pt @@ -0,0 +1,7 @@ +tray.action.manage=Aplicativos +tray.action.exit=Fechar +tray.action.about=Sobre +tray.action.refreshing=Recarregando +tray.settings=Configurações +notification.update={} atualização disponível +notification.updates={} atualizações disponíveis \ No newline at end of file diff --git a/bauh/view/resources/locale/tray/ru b/bauh/view/resources/locale/tray/ru new file mode 100644 index 00000000..10f42800 --- /dev/null +++ b/bauh/view/resources/locale/tray/ru @@ -0,0 +1,7 @@ +tray.action.manage=Приложения +tray.action.exit=Выход +tray.action.about=О программе +tray.action.refreshing=Обновляется +tray.settings=Настройки +notification.update={} обновления доступно +notification.updates={} обновлений доступно \ No newline at end of file diff --git a/bauh/view/util/cache.py b/bauh/view/util/cache.py index df01734f..b4ac1533 100644 --- a/bauh/view/util/cache.py +++ b/bauh/view/util/cache.py @@ -97,7 +97,7 @@ class CacheCleaner(Thread): class DefaultMemoryCacheFactory(MemoryCacheFactory): - def __init__(self, expiration_time: int, cleaner: CacheCleaner): + def __init__(self, expiration_time: int, cleaner: CacheCleaner = None): """ :param expiration_time: default expiration time for all instantiated caches :param cleaner @@ -108,5 +108,8 @@ class DefaultMemoryCacheFactory(MemoryCacheFactory): def new(self, expiration: int = None) -> MemoryCache: instance = DefaultMemoryCache(expiration if expiration is not None else self.expiration_time) - self.cleaner.register(instance) + + if self.cleaner: + self.cleaner.register(instance) + return instance diff --git a/bauh/view/util/disk.py b/bauh/view/util/disk.py index 4a0af2bd..e25ce42c 100644 --- a/bauh/view/util/disk.py +++ b/bauh/view/util/disk.py @@ -14,13 +14,12 @@ from bauh.api.abstract.model import SoftwarePackage class AsyncDiskCacheLoader(Thread, DiskCacheLoader): - def __init__(self, enabled: bool, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger): + def __init__(self, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger): super(AsyncDiskCacheLoader, self).__init__(daemon=True) self.pkgs = [] self._work = True self.lock = Lock() self.cache_map = cache_map - self.enabled = enabled self.logger = logger self.processed = 0 @@ -30,64 +29,62 @@ class AsyncDiskCacheLoader(Thread, DiskCacheLoader): :param pkg: :return: """ - if self.enabled and pkg and pkg.supports_disk_cache(): + if pkg and pkg.supports_disk_cache(): self.pkgs.append(pkg) def stop_working(self): self._work = False def run(self): - if self.enabled: - last = 0 + last = 0 - while True: - time.sleep(0.00001) - if len(self.pkgs) > self.processed: - pkg = self.pkgs[last] + while True: + time.sleep(0.00001) + if len(self.pkgs) > self.processed: + pkg = self.pkgs[last] - self._fill_cached_data(pkg) - self.processed += 1 - last += 1 - elif not self._work: - break + self._fill_cached_data(pkg) + self.processed += 1 + last += 1 + elif not self._work: + break def _fill_cached_data(self, pkg: SoftwarePackage) -> bool: - if self.enabled: - if os.path.exists(pkg.get_disk_data_path()): - disk_path = pkg.get_disk_data_path() - ext = disk_path.split('.')[-1] + if os.path.exists(pkg.get_disk_data_path()): + disk_path = pkg.get_disk_data_path() + ext = disk_path.split('.')[-1] - with open(disk_path) as f: - if ext == 'json': - cached_data = json.loads(f.read()) - elif ext in {'yml', 'yaml'}: - cached_data = yaml.load(f.read()) - else: - raise Exception('The cached data file {} has an unsupported format'.format(disk_path)) + with open(disk_path) as f: + if ext == 'json': + cached_data = json.loads(f.read()) + elif ext in {'yml', 'yaml'}: + cached_data = yaml.load(f.read()) + else: + raise Exception('The cached data file {} has an unsupported format'.format(disk_path)) - if cached_data: - pkg.fill_cached_data(cached_data) - cache = self.cache_map.get(pkg.__class__) + if cached_data: + pkg.fill_cached_data(cached_data) + cache = self.cache_map.get(pkg.__class__) - if cache: - cache.add_non_existing(pkg.id, cached_data) + if cache: + cache.add_non_existing(str(pkg.id), cached_data) + + return True - return True return False class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory): - def __init__(self, disk_cache_enabled: bool, logger: logging.Logger): + def __init__(self, logger: logging.Logger): super(DefaultDiskCacheLoaderFactory, self).__init__() - self.disk_cache_enabled = disk_cache_enabled self.logger = logger self.cache_map = {} def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache): if pkg_type: if pkg_type not in self.cache_map: - self.cache_map[pkg_type] = cache + self.cache_map[pkg_type] = cache def new(self) -> AsyncDiskCacheLoader: - return AsyncDiskCacheLoader(enabled=self.disk_cache_enabled, cache_map=self.cache_map, logger=self.logger) + return AsyncDiskCacheLoader(cache_map=self.cache_map, logger=self.logger) diff --git a/bauh/view/util/translation.py b/bauh/view/util/translation.py index b3d0da13..665e395a 100644 --- a/bauh/view/util/translation.py +++ b/bauh/view/util/translation.py @@ -1,10 +1,8 @@ import glob import locale -import traceback +import os from typing import Tuple, Set -from colorama import Fore - from bauh.view.util import resource @@ -43,7 +41,7 @@ class I18n(dict): def get_available_keys() -> Set[str]: locale_dir = resource.get_path('locale') - return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*')} + return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*') if os.path.isfile(file)} def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: diff --git a/bauh/view/util/util.py b/bauh/view/util/util.py index 0fc006c2..e13e21e6 100644 --- a/bauh/view/util/util.py +++ b/bauh/view/util/util.py @@ -25,25 +25,22 @@ def notify_user(msg: str, icon_path: str = None): os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_id) if icon_id else '', msg)) -def get_default_icon() -> Tuple[str, QIcon]: - system_icon = QIcon.fromTheme(__app_name__) - if not system_icon.isNull(): - return system_icon.name(), system_icon - else: - path = resource.get_path('img/logo.svg') - return path, QIcon(path) +def get_default_icon(system: bool = True) -> Tuple[str, QIcon]: + if system: + system_icon = QIcon.fromTheme(__app_name__) + if not system_icon.isNull(): + return system_icon.name(), system_icon + + path = resource.get_path('img/logo.svg') + return path, QIcon(path) -def restart_app(show_panel: bool): +def restart_app(): """ :param show_panel: if the panel should be displayed after the app restart :return: """ restart_cmd = [sys.executable, *sys.argv] - - if show_panel: - restart_cmd.append('--show-panel') - subprocess.Popen(restart_cmd) QCoreApplication.exit() diff --git a/pictures/aur/search.gif b/pictures/arch/search.gif similarity index 100% rename from pictures/aur/search.gif rename to pictures/arch/search.gif diff --git a/setup.py b/setup.py index 24c0fa53..9d50b1cc 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ import os + from setuptools import setup, find_packages DESCRIPTION = ( - "Graphical interface to manage Flatpak, Snap, AppImage, AUR and Web applications" + "Graphical interface to manage Linux applications (AppImage, Arch / AUR, Flatpak, Snap and Web)" ) AUTHOR = "bauh developers" @@ -31,12 +32,14 @@ setup( python_requires=">=3.5", url=URL, packages=find_packages(exclude=["tests.*", "tests"]), - package_data={NAME: ["view/resources/locale/*", "view/resources/img/*", "gems/*/resources/img/*", "gems/*/resources/locale/*"]}, + package_data={NAME: ["view/resources/locale/*", "view/resources/img/*", "gems/*/resources/img/*", "gems/*/resources/locale/*", "desktop/*"]}, install_requires=requirements, test_suite="tests", entry_points={ "console_scripts": [ - "{name}={name}.app:main".format(name=NAME) + "{name}={name}.app:main".format(name=NAME), + "{name}-tray={name}.app:tray".format(name=NAME), + "{name}-cli={name}.cli.app:main".format(name=NAME) ] }, include_package_data=True, @@ -46,6 +49,7 @@ setup( 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7' + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8' ] -) \ No newline at end of file +) diff --git a/tests/gems/arch/test_controller.py b/tests/gems/arch/test_controller.py deleted file mode 100644 index 1642fbc3..00000000 --- a/tests/gems/arch/test_controller.py +++ /dev/null @@ -1,168 +0,0 @@ -from unittest import TestCase - -from bauh.gems.arch.controller import ArchManager -from bauh.gems.arch.model import ArchPackage - - -class ArchManagerSortUpdateOrderTest(TestCase): - - def test__sort_deps__not_related_packages(self): - deps = { - ArchPackage(name='google-chrome', package_base='google-chrome'): {'alsa-lib', 'gtk3', 'libcups'}, - ArchPackage(name='git-cola', package_base='git-cola'): {'git', 'python-pyqt5', 'icu qt5-svg'}, - ArchPackage(name='kazam', package_base='kazam'): {'python', 'python-cairo'} - } - - sorted_list = ArchManager._sort_deps(deps, {d.name: d for d in deps}) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - def test__sort_deps__all_packages_no_deps(self): - deps = { - ArchPackage(name='xpto', package_base='xpto'): set(), - ArchPackage(name='abc', package_base='abc'): None - } - - sorted_list = ArchManager._sort_deps(deps, {d.name: d for d in deps}) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - def test__sort_deps__one_of_three_related(self): - deps = { - ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, - ArchPackage(name='def', package_base='def'): {'jkl'}, - ArchPackage(name='ghi', package_base='ghi'): {} - } - - name_map = {d.name: d for d in deps} - for _ in range(5): # testing n times to see if the same result is produced - sorted_list = ArchManager._sort_deps(deps, name_map) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - ghi = [p for p in sorted_list if p.name == 'ghi'] - self.assertEqual(1, len(ghi)) - - ghi_idx = sorted_list.index(ghi[0]) - - abc = [p for p in sorted_list if p.name == 'abc'] - self.assertEqual(1, len(abc)) - - abc_idx = sorted_list.index(abc[0]) - self.assertGreater(abc_idx, ghi_idx) - - def test__sort_deps__two_of_three_related(self): - """ - dep order = abc -> ghi -> def - expected: def, ghi, abc - """ - deps = { - ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, - ArchPackage(name='def', package_base='def'): {'jkl'}, - ArchPackage(name='ghi', package_base='ghi'): {'def'} - } - - name_map = {d.name: d for d in deps} - for _ in range(5): # testing n times to see if the same result is produced - sorted_list = ArchManager._sort_deps(deps, name_map) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - self.assertEqual(sorted_list[0].name, 'def') - self.assertEqual(sorted_list[1].name, 'ghi') - self.assertEqual(sorted_list[2].name, 'abc') - - def test__sort_deps__two_relying_on_the_same_package(self): - """ - dep order: - abc -> ghi - jkl -> ghi - ghi -> def - def -> mno - expected: def, ghi, (abc | jkl ) - """ - - deps = { - ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, - ArchPackage(name='def', package_base='def'): {'mno'}, - ArchPackage(name='ghi', package_base='ghi'): {'def'}, - ArchPackage(name='jkl', package_base='jkl'): {'ghi'} - } - - name_map = {d.name: d for d in deps} - for _ in range(5): # testing n times to see if the same result is produced - sorted_list = ArchManager._sort_deps(deps, name_map) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - self.assertEqual(sorted_list[0].name, 'def') - self.assertEqual(sorted_list[1].name, 'ghi') - - self.assertNotEqual(sorted_list[2].name, sorted_list[3].name) - self.assertIn(sorted_list[2].name, {'abc', 'jkl'}) - self.assertIn(sorted_list[3].name, {'abc', 'jkl'}) - - def test__sort_deps__with_cycle(self): - """ - dep order: - abc -> def -> ghi -> jkl -> abc - """ - deps = { - ArchPackage(name='abc', package_base='abc'): {'def'}, - ArchPackage(name='def', package_base='def'): {'ghi'}, - ArchPackage(name='ghi', package_base='ghi'): {'jkl'}, - ArchPackage(name='jkl', package_base='jkl'): {'abc'} - } - - sorted_list = ArchManager._sort_deps(deps, {d.name: d for d in deps}) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - def test__sort_deps__a_declared_dep_provided_as_a_different_name(self): - """ - dep order: - abc -> fed - def (fed) - ghi -> abc - expected: def, abc, ghi - """ - def_pkg = ArchPackage(name='def', package_base='def') - - deps = { - ArchPackage(name='abc', package_base='abc'): {'fed'}, - def_pkg: {}, - ArchPackage(name='ghi', package_base='ghi'): {'abc'} - } - - name_map = {d.name: d for d in deps} - name_map['fed'] = def_pkg - - for _ in range(5): - sorted_list = ArchManager._sort_deps(deps, name_map) - self.assertIsInstance(sorted_list, list) - self.assertEqual(len(deps), len(sorted_list)) - - for pkg in sorted_list: - self.assertIn(pkg, deps) - - self.assertEqual(sorted_list[0].name, 'def') - self.assertEqual(sorted_list[1].name, 'abc') - self.assertEqual(sorted_list[2].name, 'ghi') diff --git a/tests/gems/arch/test_sorting.py b/tests/gems/arch/test_sorting.py new file mode 100644 index 00000000..180ad39c --- /dev/null +++ b/tests/gems/arch/test_sorting.py @@ -0,0 +1,205 @@ +from unittest import TestCase + +from bauh.gems.arch import sorting + + +class SortingTest(TestCase): + + def test_sort__aur_not_related(self): + pkgs = {'google-chrome': {'d': {'alsa-lib', 'gtk3', 'libcups'}, 'p': {'google-chrome': 'google-chrome'}, 'r': 'extra'}, + 'git-cola': {'d': {'git', 'python-pyqt5', 'icu qt5-svg'}, 'p': {'git-cola': 'git-cola'}, 'r': 'extra'}, + 'kazam': {'d': {'python', 'python-cairo'}, 'p': {'kazam': 'kazam'}, 'r': 'extra'}} + + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + def test_sort__all_packages_no_deps(self): + pkgs = {'xpto': {'d': set(), 'p': {'xpto': 'xpto'}, 'r': 'extra'}, + 'abc': {'d': None, 'p': {'abc': 'abc'}, 'r': 'extra'}} + + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + def test_sort__one_of_three_related(self): + pkgs = {'def': {'d': {'jkl'}, 'p': {'def': 'def'}, 'r': 'extra'}, + 'abc': {'d': {'ghi', 'xpto'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': None, 'p': {'ghi': 'ghi'}, 'r': 'extra'}} + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + ghi = [p for p in sorted_list if p[0] == 'ghi'] + self.assertEqual(1, len(ghi)) + + ghi_idx = sorted_list.index(ghi[0]) + + abc = [p for p in sorted_list if p[0] == 'abc'] + self.assertEqual(1, len(abc)) + + abc_idx = sorted_list.index(abc[0]) + self.assertGreater(abc_idx, ghi_idx) + + def test_sort__two_of_three_related(self): + """ + dep order = abc -> ghi -> def + expected: def, ghi, abc + """ + pkgs = {'def': {'d': {'jkl'}, 'p': {'def': 'def'}, 'r': 'extra'}, + 'abc': {'d': {'ghi', 'xpto'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': {'def'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}} + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual(sorted_list[0][0], 'def') + self.assertEqual(sorted_list[1][0], 'ghi') + self.assertEqual(sorted_list[2][0], 'abc') + + def test_sort__all_related(self): + pkgs = {'lutris': {'d': {'unzip', 'python-requests'}, 'p': {'lutris': 'lutris'}, 'r': 'community'}, + 'python-requests': {'d': {'python-urllib3', 'python-chardet'}, + 'p': {'python-requests': 'python-requests'}, 'r': 'extra'}, + 'python-chardet': {'d': {'python-setuptools'}, 'p': {'python-chardet': 'python-chardet'}, + 'r': 'extra'}} + + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual('python-chardet', sorted_list[0][0]) + self.assertEqual('python-requests', sorted_list[1][0]) + self.assertEqual('lutris', sorted_list[2][0]) + + def test_sorting__pkg_should_be_after_its_latest_dependency(self): + pkgs = {'abc': {'d': {'def', 'ghi'}, 'p': {'abc': 'abc'}, 'r': 'community'}, + 'ghi': {'d': {'xpto'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}, + 'xpto': {'d': {'zzz'}, 'p': {'xpto': 'xpto'}, 'r': 'extra'}, + 'def': {'d': None, 'p': {'def': 'def'}, 'r': 'extra'}} + + for _ in range(5): # to ensure the result is always the same + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual('def', sorted_list[0][0]) + self.assertEqual('xpto', sorted_list[1][0]) + self.assertEqual('ghi', sorted_list[2][0]) + self.assertEqual('abc', sorted_list[3][0]) + + def test_sort__two_relying_on_the_same_package(self): + """ + dep order: + abc -> ghi + jkl -> ghi + ghi -> def + def -> mno + expected: def, ghi, (abc | jkl ) + """ + pkgs = {'def': {'d': {'mno'}, 'p': {'def': 'def'}, 'r': 'extra'}, + 'abc': {'d': {'ghi', 'xpto'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': {'def'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}, + 'jkl': {'d': {'ghi'}, 'p': {'jkl': 'jkl'}, 'r': 'extra'}} + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual(sorted_list[0][0], 'def') + self.assertEqual(sorted_list[1][0], 'ghi') + + self.assertNotEqual(sorted_list[2][0], sorted_list[3][0]) + self.assertIn(sorted_list[2][0], {'abc', 'jkl'}) + self.assertIn(sorted_list[3][0], {'abc', 'jkl'}) + + def test_sort__with_cycle(self): + """ + dep order: + abc -> def -> ghi -> jkl -> abc + """ + pkgs = {'def': {'d': {'ghi'}, 'p': {'def': 'def'}, 'r': 'extra'}, + 'abc': {'d': {'def'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': {'jkl'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}, + 'jkl': {'d': {'abc'}, 'p': {'jkl': 'jkl'}, 'r': 'extra'}} + + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + def test_sort__dep_provided_as_a_different_name(self): + """ + dep order: + abc -> fed + def (fed) + ghi -> abc + expected: def, abc, ghi + """ + pkgs = {'def': {'d': None, 'p': {'def': 'def', 'fed': 'def'}, 'r': 'extra'}, + 'abc': {'d': {'fed'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': {'abc'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}} + + for _ in range(5): + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual(sorted_list[0][0], 'def') + self.assertEqual(sorted_list[1][0], 'abc') + self.assertEqual(sorted_list[2][0], 'ghi') + + def test_sort__aur_pkgs_should_be_always_in_the_end(self): + """ + dep order: + abc -> fed + def (fed) + ghi -> abc + expected: def, abc, ghi + """ + pkgs = {'def': {'d': None, 'p': {'def': 'def'}, 'r': 'arch'}, + 'abc': {'d': {'ghi'}, 'p': {'abc': 'abc'}, 'r': 'extra'}, + 'ghi': {'d': {'xxx'}, 'p': {'ghi': 'ghi'}, 'r': 'extra'}} + + for _ in range(5): + sorted_list = sorting.sort(pkgs.keys(), pkgs) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(pkgs), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg[0], pkgs) + + self.assertEqual(sorted_list[0][0], 'ghi') + self.assertEqual(sorted_list[1][0], 'abc') + self.assertEqual(sorted_list[2][0], 'def')