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)
-
-
+
+
+
### 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
-
+
+
+
+
+- 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
-
+
+
+
+
+- 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
-
+
+
+
+- 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
-
-
-
-- 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
-
+
+
+
+
- It offers the possibility to customize the generated app the way you want:
-
+
+
+
+
- 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.
-
+
+
+
+
- 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.
-
+
+
+
+
+
+- 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 53088ba3..00000000
Binary files a/aur/icons/128x128.png and /dev/null differ
diff --git a/aur/icons/12x12.png b/aur/icons/12x12.png
deleted file mode 100644
index 8502307c..00000000
Binary files a/aur/icons/12x12.png and /dev/null differ
diff --git a/aur/icons/16x16.png b/aur/icons/16x16.png
deleted file mode 100644
index b8b476ca..00000000
Binary files a/aur/icons/16x16.png and /dev/null differ
diff --git a/aur/icons/24x24.png b/aur/icons/24x24.png
deleted file mode 100644
index 2f4eb525..00000000
Binary files a/aur/icons/24x24.png and /dev/null differ
diff --git a/aur/icons/256x256.png b/aur/icons/256x256.png
deleted file mode 100644
index df7936d7..00000000
Binary files a/aur/icons/256x256.png and /dev/null differ
diff --git a/aur/icons/32x32.png b/aur/icons/32x32.png
deleted file mode 100644
index c84ecae9..00000000
Binary files a/aur/icons/32x32.png and /dev/null differ
diff --git a/aur/icons/48x48.png b/aur/icons/48x48.png
deleted file mode 100644
index 0d80ac7a..00000000
Binary files a/aur/icons/48x48.png and /dev/null differ
diff --git a/aur/icons/512x512.png b/aur/icons/512x512.png
deleted file mode 100644
index 38b946cd..00000000
Binary files a/aur/icons/512x512.png and /dev/null differ
diff --git a/aur/icons/64x64.png b/aur/icons/64x64.png
deleted file mode 100644
index 01de69e7..00000000
Binary files a/aur/icons/64x64.png and /dev/null differ
diff --git a/aur/icons/8x8.png b/aur/icons/8x8.png
deleted file mode 100644
index e73905c5..00000000
Binary files a/aur/icons/8x8.png and /dev/null differ
diff --git a/aur/icons/96x96.png b/aur/icons/96x96.png
deleted file mode 100644
index 9de5638d..00000000
Binary files a/aur/icons/96x96.png and /dev/null differ
diff --git a/bauh/__init__.py b/bauh/__init__.py
index 07a0597f..1bfebade 100644
--- a/bauh/__init__.py
+++ b/bauh/__init__.py
@@ -1,5 +1,6 @@
-__version__ = '0.8.5'
+__version__ = '0.9.0'
__app_name__ = 'bauh'
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
+LOGS_PATH = '/tmp/{}/logs'.format(__app_name__)
\ No newline at end of file
diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py
index 4fcc5adb..b14c9b11 100644
--- a/bauh/api/abstract/context.py
+++ b/bauh/api/abstract/context.py
@@ -10,11 +10,10 @@ from bauh.view.util.translation import I18n
class ApplicationContext:
- def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
+ def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str):
"""
- :param disk_cache: if package data should be cached to disk
:param download_icons: if packages icons should be downloaded
:param http_client: a shared instance of http client
:param app_root_dir: GUI root dir
@@ -25,8 +24,8 @@ class ApplicationContext:
:param file_downloader
:param distro
:param app_name
+ :param root_password
"""
- self.disk_cache = disk_cache
self.download_icons = download_icons
self.http_client = http_client
self.app_root_dir = app_root_dir
@@ -40,6 +39,7 @@ class ApplicationContext:
self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility')
self.app_name = app_name
+ self.root_password = None
def is_system_x86_64(self):
return self.arch_x86_64
diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py
index d06ede8e..abf08b5d 100644
--- a/bauh/api/abstract/controller.py
+++ b/bauh/api/abstract/controller.py
@@ -9,8 +9,9 @@ import yaml
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.disk import DiskCacheLoader
-from bauh.api.abstract.handler import ProcessWatcher
-from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction
+from bauh.api.abstract.handler import ProcessWatcher, TaskManager
+from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \
+ CustomSoftwareAction
from bauh.api.abstract.view import ViewComponent
@@ -27,6 +28,38 @@ class SearchResult:
self.total = total
+class UpgradeRequirement:
+
+ def __init__(self, pkg: SoftwarePackage, reason: str = None, required_size: int = None, extra_size: int = None):
+ """
+
+ :param pkg:
+ :param reason:
+ :param required_size: size in BYTES required to upgrade the package
+ :param extra_size: the extra size IN BYTES the upgrade will allocate in relation to the already allocated
+ """
+ self.pkg = pkg
+ self.reason = reason
+ self.required_size = required_size
+ self.extra_size = extra_size
+
+
+class UpgradeRequirements:
+
+ def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement],
+ to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[UpgradeRequirement]):
+ """
+ :param to_install: additional packages that must be installed with the upgrade
+ :param to_remove: non upgrading packages that should be removed due to conflicts with upgrading packages
+ :param to_upgrade: the final packages to update
+ :param cannot_upgrade: packages which conflict with each other
+ """
+ self.to_install = to_install
+ self.to_remove = to_remove # when an upgrading package conflicts with a not upgrading package ( check all the non-upgrading packages deps an add here [including those selected to upgrade as well]
+ self.to_upgrade = to_upgrade
+ self.cannot_upgrade = cannot_upgrade
+
+
class SoftwareManager(ABC):
"""
@@ -82,27 +115,19 @@ class SoftwareManager(ABC):
if pkg.supports_disk_cache() and os.path.exists(pkg.get_disk_cache_path()):
shutil.rmtree(pkg.get_disk_cache_path())
- def sort_update_order(self, pkgs: List[SoftwarePackage]) -> 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 @@
+
+
+
+
\ 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.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 @@
+
+
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 @@
-
+
\ 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 @@
-
+
\ 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 @@
-
+
\ 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')