├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── aur ├── panel_entry.py └── tray_entry.py ├── fpakman ├── __init__.py ├── app.py ├── core │ ├── __init__.py │ ├── constants.py │ ├── controller.py │ ├── disk.py │ ├── exception.py │ ├── flatpak │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── controller.py │ │ ├── flatpak.py │ │ ├── model.py │ │ └── worker.py │ ├── model.py │ ├── resource.py │ ├── snap │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── controller.py │ │ ├── model.py │ │ ├── snap.py │ │ └── worker.py │ ├── system.py │ └── worker.py ├── resources │ ├── img │ │ ├── about.svg │ │ ├── app_info.svg │ │ ├── app_settings.svg │ │ ├── app_update.svg │ │ ├── checked.svg │ │ ├── downgrade.svg │ │ ├── exclamation.svg │ │ ├── flathub.svg │ │ ├── history.svg │ │ ├── install.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo_update.png │ │ ├── logo_update.svg │ │ ├── red_cross.svg │ │ ├── refresh.svg │ │ ├── search.svg │ │ ├── snapcraft.png │ │ ├── uninstall.svg │ │ ├── update_green.svg │ │ └── update_logo.svg │ └── locale │ │ ├── en │ │ ├── es │ │ └── pt ├── util │ ├── __init__.py │ ├── cache.py │ ├── memory.py │ └── util.py └── view │ ├── __init__.py │ └── qt │ ├── __init__.py │ ├── about.py │ ├── apps_table.py │ ├── dialog.py │ ├── history.py │ ├── info.py │ ├── root.py │ ├── systray.py │ ├── thread.py │ ├── view_model.py │ └── window.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | *.pyc 3 | .idea 4 | __pycache__ 5 | *.orig 6 | dist 7 | *.egg-info 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [0.5.1] - 2019-08-09 8 | ### Improvements: 9 | - suggestions are now retrieved asynchronously taking 45% less time. 10 | - search response takes an average of 20% less time ( reaching 35% for several results ) 11 | - app boot takes 98% less time when snapd is installed, but disabled 12 | - FPAKMAN_TRAY (--tray) is not enabled by default (0). 13 | ### Fixes 14 | - not showing correctly the latest flatpak app versions when bringing the search results 15 | - [flatpak dependency](https://github.com/vinifmor/fpakman/issues/36) 16 | 17 | ## [0.5.0] - 2019-08-06 18 | ### Improvements 19 | - search results sorting takes an average of 35% less time, reaching 60% in some scenarios 20 | - app boot takes an average of 80% less time 21 | - installed / uninstalled icons replaced by install / uninstall button 22 | - check update button design changed 23 | - app information is now available through an "Info" button 24 | - app extra actions are now available through a "Settings" button instead of a right-click 25 | - a confirmation popup is shown when the "install" / "upgrade all" button is clicked 26 | - "About" icon added to the manage panel 27 | - "Updates" icon left-aligned 28 | - show application suggestions when no app is installed. Can be enabled / disabled through the environment variable / parameter FPAKMAN_SUGGESTIONS (--sugs) 29 | - showing a warning popup after initialization when no Flatpak remotes are set 30 | - new environment variable / argument to enable / disable the tray icon and update-check daemon: FPAKMAN_TRAY (--tray) 31 | - minor GUI improvements 32 | ### Fixes: 33 | - apps table not showing empty descriptions 34 | - replacing default app icons by null icons 35 | - maximized window resize fix when filtering apps 36 | - changing table state after retrieving app history / information or an error happens 37 | - i18n 38 | 39 | ### Comments: 40 | - not adding Flatpak default remote (flathub) when initializing. It requires root actions, and will be addressed in a different way in future releases. 41 | 42 | ## [0.4.2] - 2019-07-28 43 | ### Improvements 44 | - showing a warning dialog when the application starts and **snapd** is unavailable 45 | ### Fixes: 46 | - [Snaps read index error](https://github.com/vinifmor/fpakman/issues/30) 47 | 48 | ## [0.4.1] - 2019-07-25 49 | ### Improvements 50 | - retrieving some Snaps data (icon, description and confinement) through Ubuntu API instead of Snap Store 51 | ### Fixes: 52 | - not showing app disk cached description when listing installed apps 53 | - not updating disk cached data of installed apps 54 | 55 | ## [0.4.0] - 2019-07-25 56 | ### Features 57 | - supporting snaps 58 | - search filters by application type and updates 59 | - "Refresh" option when right-clicking an installed snap application (see **Comments**) 60 | - snap / flatpak usage can be enabled / disabled via this new environment variables and arguments: FPAKMAN_FLATPAK (--flatpak), FPAKMAN_SNAP (--snap) 61 | ### Improvements 62 | - automatically shows all updates after refreshing 63 | - more accurate search results. 64 | - system notifications when an application is installed, removed or downgraded. Also when an error occurs. 65 | - showing management panel when right-clicking the tray icon. 66 | - "Updates" label replaced by an exclamation icon and moved to the right. 67 | - new environments variables / arguments associated with performance: FPAKMAN_DOWNLOAD_ICONS (--download-icons), FPAKMAN_CHECK_PACKAGING_ONCE (--check-packaging-once) 68 | - minor GUI improvements 69 | ### Comments 70 | - currently snap daemon (2.40) automatically upgrades your installed applications in background. Although it's possible to check for new updates 71 | programmatically, it requires root access and would mess up with the user experience if every 5 minutes the application asked for the password. But not to let the 72 | user with empty hands, it was added a "Refresh" option when right-clicking an installed snap application. It will update the application if its not already updated by the daemon. 73 | 74 | ## [0.3.1] - 2019-07-13 75 | ### Improvements 76 | - Console output now is optional and not shown by default 77 | - Search bar is cleaned when 'Refresh' is clicked 78 | - Full Flatpak database is not loaded during initialization: speeds up the process and reduces memory usage 79 | - Applications data not available offline are now retrieved from Flathub API on demand and cached in memory and disk (only installed) 80 | - In-memory cached data have an expiration time and are cleaned overtime to reduce memory usage 81 | - Code was refactored to support other types of packaging in the future (e.g: snap) 82 | - flatpak is not a requirement anymore 83 | - the amount of columns of the applications table was reduced to improve the user experience 84 | - new environment variables and arguments: FPAKMAN_ICON_EXPIRATION (--icon-exp), FPAKMAN_DISK_CACHE (--disk-cache) 85 | - minor GUI improvements 86 | 87 | ### Fixes: 88 | - flatpak 1.0.X: search is now retrieving the application name 89 | - app crashes when there is no internet connection while initializing, downgrading or retrieving app history 90 | 91 | ## [0.3.0] - 2019-07-02 92 | ### Features 93 | - Applications search 94 | - Now when you right-click a selected application you can: 95 | - retrieve its information 96 | - retrieve its commit history 97 | - downgrade 98 | - install and uninstall it 99 | - "About" window available when right-clicking the tray icon. 100 | 101 | ### Improvements 102 | - Performance and memory usage 103 | - Adding tooltips to toolbar buttons 104 | - "Update ?" column renamed to "Upgrade ?" 105 | - Management panel title renamed 106 | - Showing runtime apps when no app is available 107 | - Allowing to specify a custom app translation with the environment variable **FPAKMAN_LOCALE** 108 | - Adding expiration time for cached app data. Default to 1 hour. The environment variable **FPAKMAN_CACHE_EXPIRATION** can change this value. 109 | - Now the application accepts arguments related to environment variables as well. Check 'README.md'. 110 | - Minor GUI improvements 111 | - Notifying only new updates 112 | - New icon 113 | - Progress bar 114 | 115 | ## [0.2.1] - 2019-06-24 116 | ### Features 117 | - Showing the number of apps and runtime updates available 118 | ### Fixes 119 | - Retrieving information for the same AppId from different branches. 120 | 121 | ## [0.2.0] - 2019-06-18 122 | ### Features 123 | - Management panel shows update commands streams 124 | - Management panel status label is "orange" now 125 | 126 | ### Fixes 127 | - Application name is not properly showing for Flatpak 1.2.X 128 | 129 | ## [0.1.0] - 2019-06-14 130 | ### Features 131 | - System tray icon. 132 | - Applications management window. 133 | - Support for the following locales: PT, EN, ES. 134 | - System notification for new updates. 135 | - Update applications. 136 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How can I contribute? 2 | Well, you can... 3 | * Report bugs 4 | * Add improvements 5 | * Fix bugs 6 | * Add new translations or fix the current ones 7 | 8 | # Reporting bugs 9 | The best means of reporting bugs is by following these basic guidelines: 10 | 11 | * First describe in the title of the issue tracker what's gone wrong. 12 | * In the body, explain a basic synopsis of what exactly happens, explain how you got the bug one step at a time. If you're including script output, make sure you run the script with the verbose flag `-v`. 13 | * Explain what you had expected to occur, and what really occurred. 14 | * Optionally, if you want, if you're a programmer, you can try to issue a pull request yourself that fixes the issue. 15 | 16 | # Adding improvements 17 | The way to go here is to ask yourself if the improvement would be useful for more than just a singular person, if it's for a certain use case then sure! 18 | 19 | * In any pull request, explain thoroughly what changes you made 20 | * Explain why you think these changes could be useful 21 | * If it fixes a bug, be sure to link to the issue itself. 22 | * Follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) code style to keep the code consistent. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019 Vinícius Moreira 2 | 3 | zlib/libpng license 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | – The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 18 | – Altered source versions must be plainly marked as such, and must not 19 | be misrepresented as being the original software. 20 | 21 | – This notice may not be removed or altered from any source distribution. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include fpakman/resources * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fpakman 2 | 3 | **fpakman** project was renamed as **bauh** and will be maintained at https://github.com/vinifmor/bauh. 4 | 5 | 6 | Non-official graphical user interface for Flatpak / Snap application management. It is a tray icon that let the user known when new updates are available and 7 | an application management panel where you can search, update, install and uninstall applications. 8 | 9 | ### Developed with: 10 | - Python3 and Qt5. 11 | 12 | ### Requirements 13 | - libappindicator3 ( for GTK3 desktop environments ) 14 | #### Debian-based distros 15 | - python3-venv 16 | #### Arch-based distros 17 | - python 18 | - python-requests 19 | - python-virtualenv 20 | - python-pip 21 | - python-pyqt5 22 | 23 | ### Distribution 24 | **PyPi** 25 | ``` 26 | sudo pip3 install fpakman 27 | ``` 28 | 29 | **AUR** 30 | 31 | As **fpakman** package. There is also a staging version (**fpakman-staging**) but is intended for testing and may not work properly. 32 | 33 | 34 | ### Manual installation: 35 | If you prefer a manual and isolated installation, type the following commands within the cloned project folder: 36 | ``` 37 | python3 -m venv env 38 | env/bin/pip install . 39 | env/bin/fpakman 40 | ``` 41 | 42 | ### Autostart 43 | In order to autostart the application, use your Desktop Environment settings to register it as a startup application / script ("fpakman"). 44 | 45 | 46 | ### Settings 47 | You can change some application settings via environment variables or arguments (type ```fpakman --help``` to get more information). 48 | - **FPAKMAN_UPDATE_NOTIFICATION**: enable or disable system updates notifications. Use **0** (disable) or **1** (enable, default). 49 | - **FPAKMAN_CHECK_INTERVAL**: define the updates check interval in seconds. Default: 60. 50 | - **FPAKMAN_LOCALE**: define a custom app translation for a given locale key (e.g: 'pt', 'en', 'es', ...). Default: system locale. 51 | - **FPAKMAN_CACHE_EXPIRATION**: define a custom expiration time in SECONDS for cached API data. Default: 3600 (1 hour). 52 | - **FPAKMAN_ICON_EXPIRATION**: define a custom expiration time in SECONDS for cached icons. Default: 300 (5 minutes). 53 | - **FPAKMAN_DISK_CACHE**: enables / disables disk cache. When disk cache is enabled, the installed applications data are loaded faster. Use **0** (disable) or **1** (enable, default). 54 | - **FPAKMAN_DOWNLOAD_ICONS**: Enables / disables app icons download. It may improve the application speed depending on how applications data are being retrieved. Use **0** (disable) or **1** (enable, default). 55 | - **FPAKMAN_FLATPAK**: enables / disables flatpak usage. Use **0** (disable) or **1** (enabled, default) 56 | - **FPAKMAN_SNAP**: enables / disables snap usage. Use **0** (disable) or **1** (enabled, default) 57 | - **FPAKMAN_CHECK_PACKAGING_ONCE**: If the available supported packaging types should be checked ONLY once. It improves the application speed if enabled, but can generate errors if you uninstall any packaging technology while using it, and every time a supported packaging type is installed it will only be available after a restart. Use **0** (disable, default) or **1** (enable). 58 | - **FPAKMAN_TRAY**: If the tray icon and update-check daemon should be created. Use **0** (disable, default) or **1** (enable). 59 | - **FPAKMAN_SUGGESTIONS**: If application suggestions should be displayed if no app is installed (runtimes do not count as apps). Use **0** (disable) or **1** (enable, default). 60 | 61 | ### How to improve the application performance 62 | - If you don't care about a specific packaging technology and don't want **fpakman** to deal with it, just disable it via the specific argument or environment variable. For instance, if I don't care 63 | about **snaps**, I can initialize the application setting "snap=0" (**fpakman --snap=0**). This will improve the application response time, since it won't need to do any verifications associated 64 | with the technology that I don't care every time an action is executed. 65 | - If you don't care about restarting **fpakman** every time a new supported packaging technology is installed, set "check-packaging-once=1" (**fpakman --check-packaging-once=1**). This can reduce the application response time up to 80% in some scenarios, since it won't need to recheck if the packaging type is available for every action you request. 66 | - If you don't mind to see the applications icons, you can set "download-icons=0" (**fpakman --download-icons=0**). The application may have a slight response improvement, since it will reduce the parallelism within it. 67 | - If you don't mind app suggestions, disable it (**fpakman --sugs=0**) 68 | 69 | ### Roadmap 70 | - Support for other packaging technologies 71 | - Separate modules for each packaging technology 72 | - Memory and performance improvements 73 | - Improve user experience 74 | -------------------------------------------------------------------------------- /aur/panel_entry.py: -------------------------------------------------------------------------------- 1 | # Generates a .desktop file based on the current python version. Used for AUR installation 2 | import os 3 | import sys 4 | 5 | desktop_file = """ 6 | [Desktop Entry] 7 | Type = Application 8 | Name = fpakman 9 | Categories = System; 10 | Comment = Manage your Flatpak / Snap applications 11 | Exec = {path} 12 | Icon = {lib_path}/python{version}/site-packages/fpakman/resources/img/logo.svg 13 | """ 14 | 15 | py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor) 16 | 17 | fpakman_cmd = os.getenv('FPAKMAN_PATH', '/usr/bin/fpakman') 18 | 19 | with open('fpakman.desktop', 'w+') as f: 20 | f.write(desktop_file.format(lib_path=os.getenv('FPAKMAN_LIB_PATH', '/usr/lib'), 21 | version=py_version, 22 | path=fpakman_cmd)) 23 | -------------------------------------------------------------------------------- /aur/tray_entry.py: -------------------------------------------------------------------------------- 1 | # Generates a .desktop file based on the current python version. Used for AUR installation 2 | import os 3 | import sys 4 | 5 | desktop_file = """ 6 | [Desktop Entry] 7 | Type = Application 8 | Name = fpakman (tray) 9 | Categories = System; 10 | Comment = Manage your Flatpak / Snap applications 11 | Exec = {path} 12 | Icon = {lib_path}/python{version}/site-packages/fpakman/resources/img/logo.svg 13 | """ 14 | 15 | py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor) 16 | 17 | fpakman_cmd = os.getenv('FPAKMAN_PATH', '/usr/bin/fpakman') + ' --tray=1' 18 | 19 | with open('fpakman_tray.desktop', 'w+') as f: 20 | f.write(desktop_file.format(lib_path=os.getenv('FPAKMAN_LIB_PATH', '/usr/lib'), 21 | version=py_version, 22 | path=fpakman_cmd)) 23 | 24 | 25 | with open('fpakman-tray', 'w') as f: 26 | f.write(fpakman_cmd) 27 | -------------------------------------------------------------------------------- /fpakman/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5.1' 2 | __app_name__ = 'fpakman' 3 | 4 | import os 5 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | -------------------------------------------------------------------------------- /fpakman/app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | import requests 7 | from PyQt5.QtGui import QIcon 8 | from PyQt5.QtWidgets import QApplication 9 | from colorama import Fore 10 | 11 | from fpakman import __version__, __app_name__ 12 | from fpakman.core import resource 13 | from fpakman.core.controller import GenericApplicationManager 14 | from fpakman.core.disk import DiskCacheLoaderFactory 15 | from fpakman.core.flatpak.constants import FLATPAK_CACHE_PATH 16 | from fpakman.core.flatpak.controller import FlatpakManager 17 | from fpakman.core.flatpak.model import FlatpakApplication 18 | from fpakman.core.snap.constants import SNAP_CACHE_PATH 19 | from fpakman.core.snap.controller import SnapManager 20 | from fpakman.core.snap.model import SnapApplication 21 | from fpakman.util import util 22 | from fpakman.util.cache import Cache 23 | from fpakman.util.memory import CacheCleaner 24 | from fpakman.view.qt.systray import TrayIcon 25 | from fpakman.view.qt.window import ManageWindow 26 | 27 | 28 | def log_msg(msg: str, color: int = None): 29 | 30 | if color is None: 31 | print('[{}] {}'.format(__app_name__, msg)) 32 | else: 33 | print('{}[{}] {}{}'.format(color, __app_name__, msg, Fore.RESET)) 34 | 35 | 36 | parser = argparse.ArgumentParser(prog=__app_name__, description="GUI for Flatpak applications management") 37 | parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) 38 | parser.add_argument('-e', '--cache-exp', action="store", default=int(os.getenv('FPAKMAN_CACHE_EXPIRATION', 60 * 60)), type=int, help='cached API data expiration time in SECONDS. Default: %(default)s') 39 | parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('FPAKMAN_ICON_EXPIRATION', 60 * 5)), type=int, help='cached icons expiration time in SECONDS. Default: %(default)s') 40 | parser.add_argument('-l', '--locale', action="store", default=os.getenv('FPAKMAN_LOCALE', 'en'), help='Translation key. Default: %(default)s') 41 | parser.add_argument('-i', '--check-interval', action="store", default=int(os.getenv('FPAKMAN_CHECK_INTERVAL', 60)), type=int, help='Updates check interval in SECONDS. Default: %(default)s') 42 | parser.add_argument('-n', '--update-notification', action="store", choices=[0, 1], default=os.getenv('FPAKMAN_UPDATE_NOTIFICATION', 1), type=int, help='Enables / disables system notifications for new updates. Default: %(default)s') 43 | parser.add_argument('-dc', '--disk-cache', action="store", choices=[0, 1], default=os.getenv('FPAKMAN_DISK_CACHE', 1), type=int, help='Enables / disables disk cache. When disk cache is enabled, the installed applications data are loaded faster. Default: %(default)s') 44 | parser.add_argument('-di', '--download-icons', action="store", choices=[0, 1], default=os.getenv('FPAKMAN_DOWNLOAD_ICONS', 1), type=int, help='Enables / disables app icons download. It may improve the application speed, depending of how applications data are retrieved by their extensions.') 45 | parser.add_argument('--flatpak', action="store", default=os.getenv('FPAKMAN_FLATPAK', 1), choices=[0, 1], type=int, help='Enables / disables flatpak usage. Default: %(default)s') 46 | parser.add_argument('--snap', action="store", default=os.getenv('FPAKMAN_SNAP', 1), choices=[0, 1], type=int, help='Enables / disables snap usage. Default: %(default)s') 47 | parser.add_argument('-co', '--check-packaging-once', action="store", default=os.getenv('FPAKMAN_CHECK_PACKAGING_ONCE', 0), choices=[0, 1], type=int, help='If the available supported packaging types should be checked ONLY once. It improves the application speed if enabled, but can generate errors if you uninstall any packaging technology while using it, and every time a supported packaging type is installed it will only be available after a restart. Default: %(default)s') 48 | parser.add_argument('--tray', action="store", default=os.getenv('FPAKMAN_TRAY', 0), choices=[0, 1], type=int, help='If the tray icon and update-check daemon should be created. Default: %(default)s') 49 | parser.add_argument('--sugs', action="store", default=os.getenv('FPAKMAN_SUGGESTIONS', 1), choices=[0, 1], type=int, help='If app suggestions should be displayed if no app is installed (runtimes do not count as apps). Default: %(default)s') 50 | args = parser.parse_args() 51 | 52 | if args.cache_exp < 0: 53 | log_msg("'cache-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW) 54 | 55 | if args.icon_exp < 0: 56 | log_msg("'icon-exp' set to '{}': cache will not expire.".format(args.cache_exp), Fore.YELLOW) 57 | 58 | if not args.locale.strip(): 59 | log_msg("'locale' set as '{}'. You must provide a valid one. Aborting...".format(args.locale), Fore.RED) 60 | exit(1) 61 | 62 | if args.check_interval <= 0: 63 | log_msg("'check-interval' set as '{}'. It must be >= 0. Aborting...".format(args.check_interval), Fore.RED) 64 | exit(1) 65 | 66 | if not args.flatpak: 67 | log_msg("'flatpak' is disabled.", Fore.YELLOW) 68 | 69 | if not args.snap: 70 | log_msg("'snap' is disabled.", Fore.YELLOW) 71 | 72 | if args.update_notification == 0: 73 | log_msg('updates notifications are disabled', Fore.YELLOW) 74 | 75 | if args.download_icons == 0: 76 | log_msg("'download-icons' is disabled", Fore.YELLOW) 77 | 78 | if args.check_packaging_once == 1: 79 | log_msg("'check-packaging-once' is enabled", Fore.YELLOW) 80 | 81 | if args.sugs == 0: 82 | log_msg("suggestions are disabled", Fore.YELLOW) 83 | 84 | locale_keys = util.get_locale_keys(args.locale) 85 | 86 | http_session = requests.Session() 87 | caches = [] 88 | cache_map = {} 89 | managers = [] 90 | 91 | if args.flatpak: 92 | flatpak_api_cache = Cache(expiration_time=args.cache_exp) 93 | cache_map[FlatpakApplication] = flatpak_api_cache 94 | managers.append(FlatpakManager(app_args=args, api_cache=flatpak_api_cache, disk_cache=args.disk_cache, http_session=http_session, locale_keys=locale_keys)) 95 | caches.append(flatpak_api_cache) 96 | 97 | if args.disk_cache: 98 | Path(FLATPAK_CACHE_PATH).mkdir(parents=True, exist_ok=True) 99 | 100 | if args.snap: 101 | snap_api_cache = Cache(expiration_time=args.cache_exp) 102 | cache_map[SnapApplication] = snap_api_cache 103 | managers.append(SnapManager(app_args=args, disk_cache=args.disk_cache, api_cache=snap_api_cache, http_session=http_session, locale_keys=locale_keys)) 104 | caches.append(snap_api_cache) 105 | 106 | if args.disk_cache: 107 | Path(SNAP_CACHE_PATH).mkdir(parents=True, exist_ok=True) 108 | 109 | icon_cache = Cache(expiration_time=args.icon_exp) 110 | caches.append(icon_cache) 111 | 112 | disk_loader_factory = DiskCacheLoaderFactory(disk_cache=args.disk_cache, cache_map=cache_map) 113 | manager = GenericApplicationManager(managers, disk_loader_factory=disk_loader_factory, app_args=args) 114 | manager.prepare() 115 | 116 | app = QApplication(sys.argv) 117 | app.setApplicationName(__app_name__) 118 | app.setApplicationVersion(__version__) 119 | app.setWindowIcon(QIcon(resource.get_path('img/logo.svg'))) 120 | 121 | screen_size = app.primaryScreen().size() 122 | 123 | manage_window = ManageWindow(locale_keys=locale_keys, 124 | manager=manager, 125 | icon_cache=icon_cache, 126 | disk_cache=args.disk_cache, 127 | download_icons=bool(args.download_icons), 128 | screen_size=screen_size, 129 | suggestions=args.sugs) 130 | 131 | if args.tray: 132 | trayIcon = TrayIcon(locale_keys=locale_keys, 133 | manager=manager, 134 | manage_window=manage_window, 135 | check_interval=args.check_interval, 136 | update_notification=bool(args.update_notification)) 137 | manage_window.tray_icon = trayIcon 138 | trayIcon.show() 139 | else: 140 | manage_window.refresh_apps() 141 | manage_window.show() 142 | 143 | CacheCleaner(caches).start() 144 | sys.exit(app.exec_()) 145 | -------------------------------------------------------------------------------- /fpakman/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fpakman/core/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from fpakman import __app_name__ 4 | 5 | HOME_PATH = Path.home() 6 | CACHE_PATH = '{}/.cache/{}'.format(HOME_PATH, __app_name__) 7 | -------------------------------------------------------------------------------- /fpakman/core/controller.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from argparse import Namespace 3 | from threading import Thread 4 | from typing import List, Dict 5 | 6 | from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory 7 | from fpakman.core.model import Application, ApplicationUpdate 8 | from fpakman.core.system import FpakmanProcess 9 | 10 | 11 | class ApplicationManager(ABC): 12 | 13 | def __init__(self, app_args, locale_keys: dict): 14 | self.app_args = app_args 15 | self.locale_keys = locale_keys 16 | 17 | @abstractmethod 18 | def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[Application]]: 19 | pass 20 | 21 | @abstractmethod 22 | def read_installed(self, disk_loader: DiskCacheLoader) -> List[Application]: 23 | pass 24 | 25 | @abstractmethod 26 | def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess: 27 | pass 28 | 29 | @abstractmethod 30 | def clean_cache_for(self, app: Application): 31 | pass 32 | 33 | @abstractmethod 34 | def can_downgrade(self): 35 | pass 36 | 37 | @abstractmethod 38 | def update_and_stream(self, app: Application) -> FpakmanProcess: 39 | pass 40 | 41 | @abstractmethod 42 | def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess: 43 | pass 44 | 45 | @abstractmethod 46 | def get_app_type(self): 47 | pass 48 | 49 | @abstractmethod 50 | def get_info(self, app: Application) -> dict: 51 | pass 52 | 53 | @abstractmethod 54 | def get_history(self, app: Application) -> List[dict]: 55 | pass 56 | 57 | @abstractmethod 58 | def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess: 59 | pass 60 | 61 | @abstractmethod 62 | def is_enabled(self) -> bool: 63 | pass 64 | 65 | @abstractmethod 66 | def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool): 67 | pass 68 | 69 | @abstractmethod 70 | def requires_root(self, action: str, app: Application): 71 | pass 72 | 73 | @abstractmethod 74 | def refresh(self, app: Application, root_password: str) -> FpakmanProcess: 75 | pass 76 | 77 | @abstractmethod 78 | def prepare(self): 79 | """ 80 | Callback executed before the ApplicationManager starts to work. 81 | :return: 82 | """ 83 | pass 84 | 85 | @abstractmethod 86 | def list_updates(self) -> List[ApplicationUpdate]: 87 | pass 88 | 89 | @abstractmethod 90 | def list_warnings(self) -> List[str]: 91 | pass 92 | 93 | @abstractmethod 94 | def list_suggestions(self, limit: int) -> List[Application]: 95 | pass 96 | 97 | 98 | class GenericApplicationManager(ApplicationManager): 99 | 100 | def __init__(self, managers: List[ApplicationManager], disk_loader_factory: DiskCacheLoaderFactory, app_args: Namespace): 101 | super(ApplicationManager, self).__init__() 102 | self.managers = managers 103 | self.map = {m.get_app_type(): m for m in self.managers} 104 | self.disk_loader_factory = disk_loader_factory 105 | self._enabled_map = {} if app_args.check_packaging_once else None 106 | self.thread_prepare = None 107 | 108 | def _wait_to_be_ready(self): 109 | if self.thread_prepare: 110 | self.thread_prepare.join() 111 | self.thread_prepare = None 112 | 113 | def _sort(self, apps: List[Application], word: str) -> List[Application]: 114 | exact_name_matches, contains_name_matches, others = [], [], [] 115 | 116 | for app in apps: 117 | lower_name = app.base_data.name.lower() 118 | 119 | if word == lower_name: 120 | exact_name_matches.append(app) 121 | elif word in lower_name: 122 | contains_name_matches.append(app) 123 | else: 124 | others.append(app) 125 | 126 | res = [] 127 | for app_list in (exact_name_matches, contains_name_matches, others): 128 | app_list.sort(key=lambda a: a.base_data.name.lower()) 129 | res.extend(app_list) 130 | 131 | return res 132 | 133 | def _is_enabled(self, man: ApplicationManager): 134 | if self._enabled_map is not None: 135 | enabled = self._enabled_map.get(man.get_app_type()) 136 | 137 | if enabled is None: 138 | enabled = man.is_enabled() 139 | self._enabled_map[man.get_app_type()] = enabled 140 | 141 | return enabled 142 | else: 143 | return man.is_enabled() 144 | 145 | def _search(self, word: str, man: ApplicationManager, disk_loader, res: dict): 146 | if self._is_enabled(man): 147 | apps_found = man.search(word=word, disk_loader=disk_loader) 148 | res['installed'].extend(apps_found['installed']) 149 | res['new'].extend(apps_found['new']) 150 | 151 | def search(self, word: str, disk_loader: DiskCacheLoader = None) -> Dict[str, List[Application]]: 152 | self._wait_to_be_ready() 153 | 154 | res = {'installed': [], 'new': []} 155 | 156 | norm_word = word.strip().lower() 157 | disk_loader = self.disk_loader_factory.new() 158 | disk_loader.start() 159 | 160 | threads = [] 161 | 162 | for man in self.managers: 163 | t = Thread(target=self._search, args=(norm_word, man, disk_loader, res)) 164 | t.start() 165 | threads.append(t) 166 | 167 | for t in threads: 168 | t.join() 169 | 170 | if disk_loader: 171 | disk_loader.stop = True 172 | disk_loader.join() 173 | 174 | for key in res: 175 | res[key] = self._sort(res[key], norm_word) 176 | 177 | return res 178 | 179 | def read_installed(self, disk_loader: DiskCacheLoader = None) -> List[Application]: 180 | self._wait_to_be_ready() 181 | 182 | installed = [] 183 | 184 | disk_loader = None 185 | 186 | for man in self.managers: 187 | if self._is_enabled(man): 188 | if not disk_loader: 189 | disk_loader = self.disk_loader_factory.new() 190 | disk_loader.start() 191 | 192 | installed.extend(man.read_installed(disk_loader=disk_loader)) 193 | 194 | if disk_loader: 195 | disk_loader.stop = True 196 | disk_loader.join() 197 | 198 | installed.sort(key=lambda a: a.base_data.name.lower()) 199 | 200 | return installed 201 | 202 | def can_downgrade(self): 203 | return True 204 | 205 | def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess: 206 | man = self._get_manager_for(app) 207 | 208 | if man and man.can_downgrade(): 209 | return man.downgrade_app(app, root_password) 210 | else: 211 | raise Exception("downgrade is not possible for {}".format(app.__class__.__name__)) 212 | 213 | def clean_cache_for(self, app: Application): 214 | man = self._get_manager_for(app) 215 | 216 | if man: 217 | return man.clean_cache_for(app) 218 | 219 | def update_and_stream(self, app: Application) -> FpakmanProcess: 220 | man = self._get_manager_for(app) 221 | 222 | if man: 223 | return man.update_and_stream(app) 224 | 225 | def uninstall_and_stream(self, app: Application, root_password: str) -> FpakmanProcess: 226 | man = self._get_manager_for(app) 227 | 228 | if man: 229 | return man.uninstall_and_stream(app, root_password) 230 | 231 | def install_and_stream(self, app: Application, root_password: str) -> FpakmanProcess: 232 | man = self._get_manager_for(app) 233 | 234 | if man: 235 | return man.install_and_stream(app, root_password) 236 | 237 | def get_info(self, app: Application): 238 | man = self._get_manager_for(app) 239 | 240 | if man: 241 | return man.get_info(app) 242 | 243 | def get_history(self, app: Application): 244 | man = self._get_manager_for(app) 245 | 246 | if man: 247 | return man.get_history(app) 248 | 249 | def get_app_type(self): 250 | return None 251 | 252 | def is_enabled(self): 253 | return True 254 | 255 | def _get_manager_for(self, app: Application) -> ApplicationManager: 256 | man = self.map[app.__class__] 257 | return man if man and self._is_enabled(man) else None 258 | 259 | def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool): 260 | if self.disk_loader_factory.disk_cache and app.supports_disk_cache(): 261 | man = self._get_manager_for(app) 262 | 263 | if man: 264 | return man.cache_to_disk(app, icon_bytes=icon_bytes, only_icon=only_icon) 265 | 266 | def requires_root(self, action: str, app: Application): 267 | man = self._get_manager_for(app) 268 | 269 | if man: 270 | return man.requires_root(action, app) 271 | 272 | def refresh(self, app: Application, root_password: str) -> FpakmanProcess: 273 | self._wait_to_be_ready() 274 | 275 | man = self._get_manager_for(app) 276 | 277 | if man: 278 | return man.refresh(app, root_password) 279 | 280 | def _prepare(self): 281 | if self.managers: 282 | for man in self.managers: 283 | if self._is_enabled(man): 284 | man.prepare() 285 | 286 | def prepare(self): 287 | self.thread_prepare = Thread(target=self._prepare) 288 | self.thread_prepare.start() 289 | 290 | def list_updates(self) -> List[ApplicationUpdate]: 291 | self._wait_to_be_ready() 292 | 293 | updates = [] 294 | 295 | if self.managers: 296 | for man in self.managers: 297 | if self._is_enabled(man): 298 | updates.extend(man.list_updates()) 299 | 300 | return updates 301 | 302 | def list_warnings(self) -> List[str]: 303 | if self.managers: 304 | warnings = None 305 | 306 | for man in self.managers: 307 | man_warnings = man.list_warnings() 308 | 309 | if man_warnings: 310 | if warnings is None: 311 | warnings = [] 312 | 313 | warnings.extend(man_warnings) 314 | 315 | return warnings 316 | 317 | def _fill_suggestions(self, suggestions: list, man: ApplicationManager, limit: int): 318 | if self._is_enabled(man): 319 | man_sugs = man.list_suggestions(limit) 320 | 321 | if man_sugs: 322 | suggestions.extend(man_sugs) 323 | 324 | def list_suggestions(self, limit: int) -> List[Application]: 325 | if self.managers: 326 | suggestions, threads = [], [] 327 | for man in self.managers: 328 | t = Thread(target=self._fill_suggestions, args=(suggestions, man, 6)) 329 | t.start() 330 | threads.append(t) 331 | 332 | for t in threads: 333 | t.join() 334 | 335 | return suggestions 336 | -------------------------------------------------------------------------------- /fpakman/core/disk.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | from threading import Thread, Lock 5 | from typing import List, Dict 6 | 7 | from fpakman.core.model import Application 8 | from fpakman.util.cache import Cache 9 | 10 | 11 | class DiskCacheLoader(Thread): 12 | 13 | def __init__(self, enabled: bool, cache_map: Dict[type, Cache], apps: List[Application] = []): 14 | super(DiskCacheLoader, self).__init__(daemon=True) 15 | self.apps = apps 16 | self.stop = False 17 | self.lock = Lock() 18 | self.cache_map = cache_map 19 | self.enabled = enabled 20 | 21 | def run(self): 22 | 23 | if self.enabled: 24 | while True: 25 | if self.apps: 26 | self.lock.acquire() 27 | app = self.apps[0] 28 | del self.apps[0] 29 | self.lock.release() 30 | self.fill_cached_data(app) 31 | elif self.stop: 32 | break 33 | 34 | def add(self, app: Application): 35 | if self.enabled: 36 | if app and app.supports_disk_cache(): 37 | self.lock.acquire() 38 | self.apps.append(app) 39 | self.lock.release() 40 | 41 | def fill_cached_data(self, app: Application): 42 | if self.enabled: 43 | if os.path.exists(app.get_disk_data_path()): 44 | with open(app.get_disk_data_path()) as f: 45 | cached_data = json.loads(f.read()) 46 | app.fill_cached_data(cached_data) 47 | self.cache_map.get(app.__class__).add_non_existing(app.base_data.id, cached_data) 48 | 49 | 50 | class DiskCacheLoaderFactory: 51 | 52 | def __init__(self, disk_cache: bool, cache_map: Dict[type, Cache]): 53 | self.disk_cache = disk_cache 54 | self.cache_map = cache_map 55 | 56 | def new(self): 57 | return DiskCacheLoader(enabled=self.disk_cache, cache_map=self.cache_map) 58 | 59 | 60 | def save(app: Application, icon_bytes: bytes = None, only_icon: bool = False): 61 | 62 | if app.supports_disk_cache(): 63 | 64 | if not only_icon: 65 | Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) 66 | data = app.get_data_to_cache() 67 | 68 | with open(app.get_disk_data_path(), 'w+') as f: 69 | f.write(json.dumps(data)) 70 | 71 | if icon_bytes: 72 | Path(app.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) 73 | 74 | with open(app.get_disk_icon_path(), 'wb+') as f: 75 | f.write(icon_bytes) 76 | -------------------------------------------------------------------------------- /fpakman/core/exception.py: -------------------------------------------------------------------------------- 1 | 2 | class NoInternetException(Exception): 3 | pass 4 | -------------------------------------------------------------------------------- /fpakman/core/flatpak/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/core/flatpak/__init__.py -------------------------------------------------------------------------------- /fpakman/core/flatpak/constants.py: -------------------------------------------------------------------------------- 1 | from fpakman.core.constants import CACHE_PATH 2 | 3 | FLATHUB_URL = 'https://flathub.org' 4 | FLATHUB_API_URL = FLATHUB_URL + '/api/v1' 5 | FLATPAK_CACHE_PATH = '{}/flatpak/installed'.format(CACHE_PATH) 6 | -------------------------------------------------------------------------------- /fpakman/core/flatpak/controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from argparse import Namespace 4 | from datetime import datetime 5 | from typing import List, Dict 6 | 7 | from fpakman.core import disk 8 | from fpakman.core.controller import ApplicationManager 9 | from fpakman.core.disk import DiskCacheLoader 10 | from fpakman.core.flatpak import flatpak 11 | from fpakman.core.flatpak.model import FlatpakApplication 12 | from fpakman.core.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader 13 | from fpakman.core.model import ApplicationData, ApplicationUpdate 14 | from fpakman.core.system import FpakmanProcess 15 | from fpakman.util.cache import Cache 16 | 17 | 18 | class FlatpakManager(ApplicationManager): 19 | 20 | def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session, locale_keys: dict): 21 | super(FlatpakManager, self).__init__(app_args=app_args, locale_keys=locale_keys) 22 | self.api_cache = api_cache 23 | self.http_session = http_session 24 | self.disk_cache = disk_cache 25 | 26 | def get_app_type(self): 27 | return FlatpakApplication 28 | 29 | def _map_to_model(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> FlatpakApplication: 30 | 31 | app = FlatpakApplication(arch=app_json.get('arch'), 32 | branch=app_json.get('branch'), 33 | origin=app_json.get('origin'), 34 | runtime=app_json.get('runtime'), 35 | ref=app_json.get('ref'), 36 | commit=app_json.get('commit'), 37 | base_data=ApplicationData(id=app_json.get('id'), 38 | name=app_json.get('name'), 39 | version=app_json.get('version'), 40 | latest_version=app_json.get('latest_version'))) 41 | app.installed = installed 42 | 43 | api_data = self.api_cache.get(app_json['id']) 44 | 45 | expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow() 46 | 47 | if not api_data or expired_data: 48 | if not app_json['runtime']: 49 | if disk_loader: 50 | disk_loader.add(app) # preloading cached disk data 51 | 52 | FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session).start() 53 | 54 | else: 55 | app.fill_cached_data(api_data) 56 | 57 | return app 58 | 59 | def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[FlatpakApplication]]: 60 | 61 | res = {'installed': [], 'new': []} 62 | apps_found = flatpak.search(word) 63 | 64 | if apps_found: 65 | already_read = set() 66 | installed_apps = self.read_installed(disk_loader=disk_loader) 67 | 68 | if installed_apps: 69 | for app_found in apps_found: 70 | for installed_app in installed_apps: 71 | if app_found['id'] == installed_app.base_data.id: 72 | res['installed'].append(installed_app) 73 | already_read.add(app_found['id']) 74 | 75 | if len(apps_found) > len(already_read): 76 | for app_found in apps_found: 77 | if app_found['id'] not in already_read: 78 | res['new'].append(self._map_to_model(app_found, False, disk_loader)) 79 | 80 | return res 81 | 82 | def read_installed(self, disk_loader: DiskCacheLoader) -> List[FlatpakApplication]: 83 | installed = flatpak.list_installed() 84 | models = [] 85 | 86 | if installed: 87 | 88 | available_updates = flatpak.list_updates_as_str() 89 | 90 | for app_json in installed: 91 | model = self._map_to_model(app_json=app_json, installed=True, disk_loader=disk_loader) 92 | model.update = app_json['id'] in available_updates 93 | models.append(model) 94 | 95 | return models 96 | 97 | def can_downgrade(self): 98 | return True 99 | 100 | def downgrade_app(self, app: FlatpakApplication, root_password: str) -> FpakmanProcess: 101 | 102 | commits = flatpak.get_app_commits(app.ref, app.origin) 103 | 104 | commit_idx = commits.index(app.commit) 105 | 106 | # downgrade is not possible if the app current commit in the first one: 107 | if commit_idx == len(commits) - 1: 108 | return None 109 | 110 | return FpakmanProcess(subproc=flatpak.downgrade_and_stream(app.ref, commits[commit_idx + 1], root_password), 111 | success_phrase='Updates complete.') 112 | 113 | def clean_cache_for(self, app: FlatpakApplication): 114 | self.api_cache.delete(app.base_data.id) 115 | 116 | if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()): 117 | shutil.rmtree(app.get_disk_cache_path()) 118 | 119 | def update_and_stream(self, app: FlatpakApplication) -> FpakmanProcess: 120 | return FpakmanProcess(subproc=flatpak.update_and_stream(app.ref)) 121 | 122 | def uninstall_and_stream(self, app: FlatpakApplication, root_password: str = None) -> FpakmanProcess: 123 | return FpakmanProcess(subproc=flatpak.uninstall_and_stream(app.ref)) 124 | 125 | def get_info(self, app: FlatpakApplication) -> dict: 126 | app_info = flatpak.get_app_info_fields(app.base_data.id, app.branch) 127 | app_info['name'] = app.base_data.name 128 | app_info['type'] = 'runtime' if app.runtime else 'app' 129 | app_info['description'] = app.base_data.description 130 | return app_info 131 | 132 | def get_history(self, app: FlatpakApplication) -> List[dict]: 133 | return flatpak.get_app_commits_data(app.ref, app.origin) 134 | 135 | def install_and_stream(self, app: FlatpakApplication, root_password: str) -> FpakmanProcess: 136 | return FpakmanProcess(subproc=flatpak.install_and_stream(app.base_data.id, app.origin)) 137 | 138 | def is_enabled(self): 139 | return flatpak.is_installed() 140 | 141 | def cache_to_disk(self, app: FlatpakApplication, icon_bytes: bytes, only_icon: bool): 142 | if self.disk_cache and app.supports_disk_cache(): 143 | disk.save(app, icon_bytes, only_icon) 144 | 145 | def requires_root(self, action: str, app: FlatpakApplication): 146 | return action == 'downgrade' 147 | 148 | def refresh(self, app: FlatpakApplication, root_password: str) -> FpakmanProcess: 149 | raise Exception("'refresh' is not supported for {}".format(app.__class__.__name__)) 150 | 151 | def prepare(self): 152 | pass 153 | 154 | def list_updates(self) -> List[ApplicationUpdate]: 155 | updates = [] 156 | installed = flatpak.list_installed(extra_fields=False) 157 | 158 | if installed: 159 | available_updates = flatpak.list_updates_as_str() 160 | 161 | if available_updates: 162 | loaders = None 163 | 164 | for app_json in installed: 165 | if app_json['id'] in available_updates: 166 | loader = FlatpakUpdateLoader(app=app_json, http_session=self.http_session) 167 | loader.start() 168 | 169 | if loaders is None: 170 | loaders = [] 171 | 172 | loaders.append(loader) 173 | 174 | if loaders: 175 | for loader in loaders: 176 | loader.join() 177 | app = loader.app 178 | updates.append(ApplicationUpdate(app_id='{}:{}'.format(app['id'], app['branch']), 179 | app_type='flatpak', 180 | version=app.get('version'))) 181 | return updates 182 | 183 | def list_warnings(self) -> List[str]: 184 | if flatpak.is_installed(): 185 | if not flatpak.has_remotes_set(): 186 | return [self.locale_keys['flatpak.notification.no_remotes']] 187 | 188 | def list_suggestions(self, limit: int) -> List[FlatpakApplication]: 189 | 190 | res = [] 191 | 192 | if limit != 0: 193 | 194 | for app_id in ('com.spotify.Client', 'com.skype.Client', 'com.dropbox.Client', 'us.zoom.Zoom', 'com.visualstudio.code', 'org.telegram.desktop', 'org.inkscape.Inkscape', 'org.libretro.RetroArch', 'org.kde.kdenlive', 'org.videolan.VLC'): 195 | 196 | app_json = flatpak.search(app_id, app_id=True) 197 | 198 | if app_json: 199 | res.append(self._map_to_model(app_json[0], False, None)) 200 | 201 | if len(res) == limit: 202 | break 203 | 204 | return res 205 | 206 | -------------------------------------------------------------------------------- /fpakman/core/flatpak/flatpak.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | from typing import List 4 | 5 | from fpakman.core import system 6 | from fpakman.core.exception import NoInternetException 7 | 8 | BASE_CMD = 'flatpak' 9 | 10 | 11 | def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict: 12 | 13 | app_array = line.split('\t') 14 | 15 | if version >= '1.3.0': 16 | app = {'name': app_array[0], 17 | 'id': app_array[1], 18 | 'version': app_array[2], 19 | 'branch': app_array[3]} 20 | 21 | elif '1.0' <= version < '1.1': 22 | 23 | ref_data = app_array[0].split('/') 24 | 25 | app = {'id': ref_data[0], 26 | 'arch': ref_data[1], 27 | 'name': ref_data[0].split('.')[-1], 28 | 'ref': app_array[0], 29 | 'options': app_array[1], 30 | 'branch': ref_data[2], 31 | 'version': None} 32 | elif '1.2' <= version < '1.3': 33 | app = {'id': app_array[1], 34 | 'name': app_array[1].strip().split('.')[-1], 35 | 'version': app_array[2], 36 | 'branch': app_array[3], 37 | 'arch': app_array[4], 38 | 'origin': app_array[5]} 39 | else: 40 | raise Exception('Unsupported version') 41 | 42 | if extra_fields: 43 | extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True) 44 | app.update(extra_fields) 45 | 46 | return app 47 | 48 | 49 | def get_app_info_fields(app_id: str, branch: str, fields: List[str] = [], check_runtime: bool = False): 50 | info = re.findall(r'\w+:\s.+', get_app_info(app_id, branch)) 51 | data = {} 52 | fields_to_retrieve = len(fields) + (1 if check_runtime and 'ref' not in fields else 0) 53 | 54 | for field in info: 55 | 56 | if fields and fields_to_retrieve == 0: 57 | break 58 | 59 | field_val = field.split(':') 60 | field_name = field_val[0].lower() 61 | 62 | if not fields or field_name in fields or (check_runtime and field_name == 'ref'): 63 | data[field_name] = field_val[1].strip() 64 | 65 | if fields: 66 | fields_to_retrieve -= 1 67 | 68 | if check_runtime and field_name == 'ref': 69 | data['runtime'] = data['ref'].startswith('runtime/') 70 | 71 | return data 72 | 73 | 74 | def is_installed(): 75 | version = get_version() 76 | return False if version is None else True 77 | 78 | 79 | def get_version(): 80 | res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False) 81 | return res.split(' ')[1].strip() if res else None 82 | 83 | 84 | def get_app_info(app_id: str, branch: str): 85 | return system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch)) 86 | 87 | 88 | def list_installed(extra_fields: bool = True) -> List[dict]: 89 | apps_str = system.run_cmd('{} list'.format(BASE_CMD)) 90 | 91 | if apps_str: 92 | version = get_version() 93 | app_lines = apps_str.split('\n') 94 | return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line] 95 | 96 | return [] 97 | 98 | 99 | def update_and_stream(app_ref: str): 100 | """ 101 | Updates the app reference and streams Flatpak output, 102 | :param app_ref: 103 | :return: 104 | """ 105 | return system.cmd_to_subprocess([BASE_CMD, 'update', '-y', app_ref]) 106 | 107 | 108 | def uninstall_and_stream(app_ref: str): 109 | """ 110 | Removes the app by its reference 111 | :param app_ref: 112 | :return: 113 | """ 114 | return system.cmd_to_subprocess([BASE_CMD, 'uninstall', app_ref, '-y']) 115 | 116 | 117 | def list_updates_as_str(): 118 | return system.run_cmd('{} update'.format(BASE_CMD), ignore_return_code=True) 119 | 120 | 121 | def downgrade_and_stream(app_ref: str, commit: str, root_password: str) -> subprocess.Popen: 122 | return system.cmd_as_root([BASE_CMD, 'update', '--commit={}'.format(commit), app_ref, '-y'], root_password) 123 | 124 | 125 | def get_app_commits(app_ref: str, origin: str) -> List[str]: 126 | log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) 127 | 128 | if log: 129 | return re.findall(r'Commit+:\s(.+)', log) 130 | else: 131 | raise NoInternetException() 132 | 133 | 134 | def get_app_commits_data(app_ref: str, origin: str) -> List[dict]: 135 | log = system.run_cmd('{} remote-info --log {} {}'.format(BASE_CMD, origin, app_ref)) 136 | 137 | if not log: 138 | raise NoInternetException() 139 | 140 | res = re.findall(r'(Commit|Subject|Date):\s(.+)', log) 141 | 142 | commits = [] 143 | 144 | commit = {} 145 | 146 | for idx, data in enumerate(res): 147 | commit[data[0].strip().lower()] = data[1].strip() 148 | 149 | if (idx + 1) % 3 == 0: 150 | commits.append(commit) 151 | commit = {} 152 | 153 | return commits 154 | 155 | 156 | def search(word: str, app_id: bool = False) -> List[dict]: 157 | cli_version = get_version() 158 | 159 | res = system.run_cmd('{} search {}'.format(BASE_CMD, word)) 160 | 161 | found = [] 162 | 163 | split_res = res.split('\n') 164 | 165 | if split_res and split_res[0].lower() != 'no matches found': 166 | for info in split_res: 167 | if info: 168 | info_list = info.split('\t') 169 | if cli_version >= '1.3.0': 170 | id_ = info_list[2].strip() 171 | 172 | if app_id and id_ != word: 173 | continue 174 | 175 | version = info_list[3].strip() 176 | app = { 177 | 'name': info_list[0].strip(), 178 | 'description': info_list[1].strip(), 179 | 'id': id_, 180 | 'version': version, 181 | 'latest_version': version, 182 | 'branch': info_list[4].strip(), 183 | 'origin': info_list[5].strip(), 184 | 'runtime': False, 185 | 'arch': None, # unknown at this moment, 186 | 'ref': None # unknown at this moment 187 | } 188 | elif cli_version >= '1.2.0': 189 | id_ = info_list[1].strip() 190 | 191 | if app_id and id_ != word: 192 | continue 193 | 194 | desc = info_list[0].split('-') 195 | version = info_list[2].strip() 196 | app = { 197 | 'name': desc[0].strip(), 198 | 'description': desc[1].strip(), 199 | 'id': id_, 200 | 'version': version, 201 | 'latest_version': version, 202 | 'branch': info_list[3].strip(), 203 | 'origin': info_list[4].strip(), 204 | 'runtime': False, 205 | 'arch': None, # unknown at this moment, 206 | 'ref': None # unknown at this moment 207 | } 208 | else: 209 | id_ = info_list[0].strip() 210 | 211 | if app_id and id_ != word: 212 | continue 213 | 214 | version = info_list[1].strip() 215 | app = { 216 | 'name': '', 217 | 'description': info_list[4].strip(), 218 | 'id': id_, 219 | 'version': version, 220 | 'latest_version': version, 221 | 'branch': info_list[2].strip(), 222 | 'origin': info_list[3].strip(), 223 | 'runtime': False, 224 | 'arch': None, # unknown at this moment, 225 | 'ref': None # unknown at this moment 226 | } 227 | 228 | found.append(app) 229 | 230 | if app_id and len(found) > 0: 231 | break 232 | 233 | return found 234 | 235 | 236 | def install_and_stream(app_id: str, origin: str): 237 | return system.cmd_to_subprocess([BASE_CMD, 'install', origin, app_id, '-y']) 238 | 239 | 240 | def set_default_remotes(): 241 | system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo') 242 | 243 | 244 | def has_remotes_set() -> bool: 245 | return bool(system.run_cmd('{} remotes'.format(BASE_CMD)).strip()) 246 | -------------------------------------------------------------------------------- /fpakman/core/flatpak/model.py: -------------------------------------------------------------------------------- 1 | from fpakman.core import resource 2 | from fpakman.core.flatpak.constants import FLATPAK_CACHE_PATH 3 | from fpakman.core.model import Application, ApplicationData 4 | 5 | 6 | class FlatpakApplication(Application): 7 | 8 | def __init__(self, base_data: ApplicationData, branch: str, arch: str, origin: str, runtime: bool, ref: str, commit: str): 9 | super(FlatpakApplication, self).__init__(base_data=base_data) 10 | self.ref = ref 11 | self.branch = branch 12 | self.arch = arch 13 | self.origin = origin 14 | self.runtime = runtime 15 | self.commit = commit 16 | 17 | def is_incomplete(self): 18 | return self.base_data.description is None and self.base_data.icon_url 19 | 20 | def has_history(self): 21 | return self.installed 22 | 23 | def has_info(self): 24 | return self.installed 25 | 26 | def can_be_downgraded(self): 27 | return self.installed 28 | 29 | def can_be_uninstalled(self): 30 | return self.installed 31 | 32 | def can_be_installed(self): 33 | return not self.installed 34 | 35 | def get_type(self): 36 | return 'flatpak' 37 | 38 | def can_be_refreshed(self): 39 | return False 40 | 41 | def get_default_icon_path(self): 42 | return resource.get_path('img/flathub.svg') 43 | 44 | def is_library(self): 45 | return self.runtime 46 | 47 | def get_disk_cache_path(self): 48 | return '{}/{}'.format(FLATPAK_CACHE_PATH, self.base_data.id) 49 | 50 | def get_data_to_cache(self): 51 | return { 52 | 'description': self.base_data.description, 53 | 'icon_url': self.base_data.icon_url, 54 | 'latest_version': self.base_data.latest_version, 55 | 'version': self.base_data.version, 56 | 'name': self.base_data.name 57 | } 58 | 59 | def fill_cached_data(self, data: dict): 60 | for attr in self.get_data_to_cache().keys(): 61 | if not getattr(self.base_data, attr): 62 | setattr(self.base_data, attr, data[attr]) 63 | -------------------------------------------------------------------------------- /fpakman/core/flatpak/worker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import traceback 3 | from threading import Thread 4 | 5 | from colorama import Fore 6 | 7 | from fpakman.core.controller import ApplicationManager 8 | from fpakman.core.flatpak import flatpak 9 | from fpakman.core.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL 10 | from fpakman.core.flatpak.model import FlatpakApplication 11 | from fpakman.core.model import ApplicationStatus 12 | from fpakman.core.worker import AsyncDataLoader 13 | from fpakman.util.cache import Cache 14 | 15 | 16 | class FlatpakAsyncDataLoader(AsyncDataLoader): 17 | 18 | def __init__(self, app: FlatpakApplication, manager: ApplicationManager, http_session, api_cache: Cache, attempts: int = 2, timeout: int = 30): 19 | super(FlatpakAsyncDataLoader, self).__init__(app=app) 20 | self.manager = manager 21 | self.http_session = http_session 22 | self.attempts = attempts 23 | self.api_cache = api_cache 24 | self.persist = False 25 | self.timeout = timeout 26 | 27 | def run(self): 28 | if self.app: 29 | self.app.status = ApplicationStatus.LOADING_DATA 30 | 31 | for _ in range(0, self.attempts): 32 | try: 33 | res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app.base_data.id), timeout=self.timeout) 34 | 35 | if res.status_code == 200 and res.text: 36 | data = res.json() 37 | 38 | if not self.app.base_data.version: 39 | self.app.base_data.version = data.get('version') 40 | 41 | if not self.app.base_data.name: 42 | self.app.base_data.name = data.get('name') 43 | 44 | self.app.base_data.description = data.get('description', data.get('summary', None)) 45 | self.app.base_data.icon_url = data.get('iconMobileUrl', None) 46 | self.app.base_data.latest_version = data.get('currentReleaseVersion', self.app.base_data.version) 47 | 48 | if not self.app.base_data.version and self.app.base_data.latest_version: 49 | self.app.base_data.version = self.app.base_data.latest_version 50 | 51 | if not self.app.installed and self.app.base_data.latest_version: 52 | self.app.base_data.version = self.app.base_data.latest_version 53 | 54 | if self.app.base_data.icon_url and self.app.base_data.icon_url.startswith('/'): 55 | self.app.base_data.icon_url = FLATHUB_URL + self.app.base_data.icon_url 56 | 57 | loaded_data = self.app.get_data_to_cache() 58 | 59 | self.api_cache.add(self.app.base_data.id, loaded_data) 60 | self.app.status = ApplicationStatus.READY 61 | self.persist = self.app.supports_disk_cache() 62 | break 63 | else: 64 | self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format( 65 | self.app.base_data.id, res.status_code, res.content.decode()), Fore.RED) 66 | except: 67 | self.log_msg("Could not retrieve app data for id '{}'".format(self.app.base_data.id), Fore.YELLOW) 68 | traceback.print_exc() 69 | time.sleep(0.5) 70 | 71 | self.app.status = ApplicationStatus.READY 72 | 73 | if self.persist: 74 | self.manager.cache_to_disk(app=self.app, icon_bytes=None, only_icon=False) 75 | 76 | def clone(self) -> "FlatpakAsyncDataLoader": 77 | return FlatpakAsyncDataLoader(manager=self.manager, 78 | api_cache=self.api_cache, 79 | attempts=self.attempts, 80 | http_session=self.http_session, 81 | timeout=self.timeout, 82 | app=self.app) 83 | 84 | 85 | class FlatpakUpdateLoader(Thread): 86 | 87 | def __init__(self, app: dict, http_session, attempts: int = 2, timeout: int = 20): 88 | super(FlatpakUpdateLoader, self).__init__(daemon=True) 89 | self.app = app 90 | self.http_session = http_session 91 | self.attempts = attempts 92 | self.timeout = timeout 93 | 94 | def run(self): 95 | 96 | if self.app.get('ref') is None: 97 | self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True)) 98 | else: 99 | self.app['runtime'] = self.app['ref'].startswith('runtime/') 100 | 101 | if not self.app['runtime']: 102 | current_attempts = 0 103 | 104 | while current_attempts < self.attempts: 105 | 106 | current_attempts += 1 107 | 108 | try: 109 | res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']), timeout=self.timeout) 110 | 111 | if res.status_code == 200 and res.text: 112 | data = res.json() 113 | 114 | if data.get('currentReleaseVersion'): 115 | self.app['version'] = data['currentReleaseVersion'] 116 | 117 | break 118 | except: 119 | traceback.print_exc() 120 | time.sleep(0.5) 121 | -------------------------------------------------------------------------------- /fpakman/core/model.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from enum import Enum 3 | 4 | from fpakman.core import resource 5 | 6 | 7 | class ApplicationStatus(Enum): 8 | READY = 1 9 | LOADING_DATA = 2 10 | 11 | 12 | class ApplicationData: 13 | 14 | def __init__(self, id: str, version: str, name: str = None, description: str = None, latest_version: str = None, icon_url: str = None): 15 | self.id = id 16 | self.name = name 17 | self.version = version 18 | self.description = description 19 | self.latest_version = latest_version 20 | self.icon_url = icon_url 21 | 22 | 23 | class Application(ABC): 24 | 25 | def __init__(self, base_data: ApplicationData, status: ApplicationStatus = ApplicationStatus.READY, installed: bool = False, update: bool = False): 26 | self.base_data = base_data 27 | self.status = status 28 | self.installed = installed 29 | self.update = update 30 | 31 | @abstractmethod 32 | def has_history(self): 33 | pass 34 | 35 | @abstractmethod 36 | def has_info(self): 37 | pass 38 | 39 | @abstractmethod 40 | def can_be_downgraded(self): 41 | pass 42 | 43 | @abstractmethod 44 | def can_be_uninstalled(self): 45 | pass 46 | 47 | @abstractmethod 48 | def can_be_installed(self): 49 | pass 50 | 51 | @abstractmethod 52 | def can_be_refreshed(self): 53 | return self.installed 54 | 55 | @abstractmethod 56 | def get_type(self): 57 | pass 58 | 59 | def get_default_icon_path(self): 60 | return resource.get_path('img/logo.svg') 61 | 62 | @abstractmethod 63 | def is_library(self): 64 | pass 65 | 66 | def supports_disk_cache(self): 67 | return self.installed and not self.is_library() 68 | 69 | @abstractmethod 70 | def get_disk_cache_path(self): 71 | pass 72 | 73 | def get_disk_icon_path(self): 74 | return '{}/icon.png'.format(self.get_disk_cache_path()) 75 | 76 | def get_disk_data_path(self): 77 | return '{}/data.json'.format(self.get_disk_cache_path()) 78 | 79 | @abstractmethod 80 | def get_data_to_cache(self): 81 | pass 82 | 83 | @abstractmethod 84 | def fill_cached_data(self, data: dict): 85 | pass 86 | 87 | def __str__(self): 88 | return '{} (id={}, name={})'.format(self.__class__.__name__, self.base_data.id, self.base_data.name) 89 | 90 | 91 | class ApplicationUpdate: 92 | 93 | def __init__(self, app_id: str, version: str, app_type: str): 94 | self.id = app_id 95 | self.version = version 96 | self.type = app_type 97 | 98 | def __str__(self): 99 | return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type) 100 | -------------------------------------------------------------------------------- /fpakman/core/resource.py: -------------------------------------------------------------------------------- 1 | 2 | from fpakman import ROOT_DIR 3 | 4 | 5 | def get_path(resource_path): 6 | return ROOT_DIR + '/resources/' + resource_path 7 | -------------------------------------------------------------------------------- /fpakman/core/snap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/core/snap/__init__.py -------------------------------------------------------------------------------- /fpakman/core/snap/constants.py: -------------------------------------------------------------------------------- 1 | from fpakman.core.constants import CACHE_PATH 2 | 3 | SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1' 4 | SNAP_CACHE_PATH = '{}/snap/installed'.format(CACHE_PATH) 5 | -------------------------------------------------------------------------------- /fpakman/core/snap/controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from argparse import Namespace 4 | from datetime import datetime 5 | from typing import Dict, List 6 | 7 | from fpakman.core import disk 8 | from fpakman.core.controller import ApplicationManager 9 | from fpakman.core.disk import DiskCacheLoader 10 | from fpakman.core.model import ApplicationData, Application, ApplicationUpdate 11 | from fpakman.core.snap import snap 12 | from fpakman.core.snap.model import SnapApplication 13 | from fpakman.core.snap.worker import SnapAsyncDataLoader 14 | from fpakman.core.system import FpakmanProcess 15 | from fpakman.util.cache import Cache 16 | 17 | 18 | class SnapManager(ApplicationManager): 19 | 20 | def __init__(self, app_args: Namespace, api_cache: Cache, disk_cache: bool, http_session, locale_keys: dict): 21 | super(SnapManager, self).__init__(app_args=app_args, locale_keys=locale_keys) 22 | self.api_cache = api_cache 23 | self.http_session = http_session 24 | self.disk_cache = disk_cache 25 | 26 | def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader) -> SnapApplication: 27 | app = SnapApplication(publisher=app_json.get('publisher'), 28 | rev=app_json.get('rev'), 29 | notes=app_json.get('notes'), 30 | app_type=app_json.get('type'), 31 | base_data=ApplicationData(id=app_json.get('name'), 32 | name=app_json.get('name'), 33 | version=app_json.get('version'), 34 | latest_version=app_json.get('version'), 35 | description=app_json.get('description') 36 | )) 37 | 38 | if app.publisher: 39 | app.publisher = app.publisher.replace('*', '') 40 | 41 | app.installed = installed 42 | 43 | api_data = self.api_cache.get(app_json['name']) 44 | expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow() 45 | 46 | if (not api_data or expired_data) and not app.is_library(): 47 | if disk_loader and app.installed: 48 | disk_loader.add(app) 49 | 50 | SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session, download_icons=self.app_args.download_icons).start() 51 | else: 52 | app.fill_cached_data(api_data) 53 | 54 | return app 55 | 56 | def search(self, word: str, disk_loader: DiskCacheLoader) -> Dict[str, List[SnapApplication]]: 57 | installed = self.read_installed(disk_loader) 58 | 59 | res = {'installed': [], 'new': []} 60 | 61 | for app_json in snap.search(word): 62 | 63 | already_installed = None 64 | 65 | if installed: 66 | already_installed = [i for i in installed if i.base_data.id == app_json.get('name')] 67 | already_installed = already_installed[0] if already_installed else None 68 | 69 | if already_installed: 70 | res['installed'].append(already_installed) 71 | else: 72 | res['new'].append(self.map_json(app_json, installed=False, disk_loader=disk_loader)) 73 | 74 | return res 75 | 76 | def read_installed(self, disk_loader: DiskCacheLoader) -> List[SnapApplication]: 77 | return [self.map_json(app_json, installed=True, disk_loader=disk_loader) for app_json in snap.read_installed()] 78 | 79 | def downgrade_app(self, app: Application, root_password: str) -> FpakmanProcess: 80 | return FpakmanProcess(subproc=snap.downgrade_and_stream(app.base_data.name, root_password), wrong_error_phrase=None) 81 | 82 | def clean_cache_for(self, app: SnapApplication): 83 | self.api_cache.delete(app.base_data.name) 84 | 85 | if app.supports_disk_cache() and os.path.exists(app.get_disk_cache_path()): 86 | shutil.rmtree(app.get_disk_cache_path()) 87 | 88 | def can_downgrade(self): 89 | return True 90 | 91 | def update_and_stream(self, app: SnapApplication) -> FpakmanProcess: 92 | pass 93 | 94 | def uninstall_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess: 95 | return FpakmanProcess(subproc=snap.uninstall_and_stream(app.base_data.name, root_password)) 96 | 97 | def get_app_type(self): 98 | return SnapApplication 99 | 100 | def get_info(self, app: SnapApplication) -> dict: 101 | info = snap.get_info(app.base_data.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed')) 102 | info['description'] = app.base_data.description 103 | info['publisher'] = app.publisher 104 | info['revision'] = app.rev 105 | info['name'] = app.base_data.name 106 | 107 | if info.get('commands'): 108 | info['commands'] = ' '.join(info['commands']) 109 | 110 | return info 111 | 112 | def get_history(self, app: Application) -> List[dict]: 113 | return [] 114 | 115 | def install_and_stream(self, app: SnapApplication, root_password: str) -> FpakmanProcess: 116 | return FpakmanProcess(subproc=snap.install_and_stream(app.base_data.name, app.confinement, root_password)) 117 | 118 | def is_enabled(self) -> bool: 119 | return snap.is_installed() 120 | 121 | def cache_to_disk(self, app: Application, icon_bytes: bytes, only_icon: bool): 122 | if self.disk_cache and app.supports_disk_cache(): 123 | disk.save(app, icon_bytes, only_icon) 124 | 125 | def requires_root(self, action: str, app: SnapApplication): 126 | return action != 'search' 127 | 128 | def refresh(self, app: SnapApplication, root_password: str) -> FpakmanProcess: 129 | return FpakmanProcess(subproc=snap.refresh_and_stream(app.base_data.name, root_password)) 130 | 131 | def prepare(self): 132 | pass 133 | 134 | def list_updates(self) -> List[ApplicationUpdate]: 135 | return [] 136 | 137 | def list_warnings(self) -> List[str]: 138 | if snap.get_snapd_version() == 'unavailable': 139 | return [self.locale_keys['snap.notification.snapd_unavailable']] 140 | 141 | def list_suggestions(self, limit: int) -> List[SnapApplication]: 142 | 143 | suggestions = [] 144 | 145 | if limit != 0: 146 | for name in ('whatsdesk', 'slack', 'yakyak', 'instagraph', 'pycharm-professional', 'eclipse', 'gimp', 'supertuxkart'): 147 | res = snap.search(name, exact_name=True) 148 | if res: 149 | suggestions.append(self.map_json(res[0], installed=False, disk_loader=None)) 150 | 151 | if len(suggestions) == limit: 152 | break 153 | 154 | return suggestions 155 | -------------------------------------------------------------------------------- /fpakman/core/snap/model.py: -------------------------------------------------------------------------------- 1 | from fpakman.core import resource 2 | from fpakman.core.model import Application, ApplicationData 3 | from fpakman.core.snap.constants import SNAP_CACHE_PATH 4 | 5 | 6 | class SnapApplication(Application): 7 | 8 | def __init__(self, base_data: ApplicationData, publisher: str, rev: str, notes: str, app_type: str, confinement: str = None): 9 | super(SnapApplication, self).__init__(base_data=base_data) 10 | self.publisher = publisher 11 | self.rev = rev 12 | self.notes = notes 13 | self.type = app_type 14 | self.confinement = confinement 15 | 16 | def has_history(self): 17 | return False 18 | 19 | def has_info(self): 20 | return True 21 | 22 | def can_be_downgraded(self): 23 | return self.installed 24 | 25 | def can_be_uninstalled(self): 26 | return self.installed 27 | 28 | def can_be_installed(self): 29 | return not self.installed 30 | 31 | def can_be_refreshed(self): 32 | return self.installed 33 | 34 | def get_type(self): 35 | return 'snap' 36 | 37 | def get_default_icon_path(self): 38 | return resource.get_path('img/snapcraft.png') 39 | 40 | def is_library(self): 41 | return self.type in ('core', 'base', 'snapd') or self.base_data.name.startswith('gtk-') or self.base_data.name.startswith('gnome-') 42 | 43 | def get_disk_cache_path(self): 44 | return '{}/{}'.format(SNAP_CACHE_PATH, self.base_data.name) 45 | 46 | def get_data_to_cache(self): 47 | return { 48 | "icon_url": self.base_data.icon_url, 49 | 'confinement': self.confinement, 50 | 'description': self.base_data.description 51 | } 52 | 53 | def fill_cached_data(self, data: dict): 54 | if data: 55 | for base_attr in ('icon_url', 'description'): 56 | if data.get(base_attr): 57 | setattr(self.base_data, base_attr, data[base_attr]) 58 | 59 | if data.get('confinement'): 60 | self.confinement = data['confinement'] 61 | -------------------------------------------------------------------------------- /fpakman/core/snap/snap.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | from typing import List 4 | 5 | from fpakman.core import system 6 | 7 | BASE_CMD = 'snap' 8 | 9 | 10 | def is_installed(): 11 | version = get_snapd_version() 12 | return False if version is None or version == 'unavailable' else True 13 | 14 | 15 | def get_version(): 16 | res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False) 17 | return res.split('\n')[0].split(' ')[-1].strip() if res else None 18 | 19 | 20 | def get_snapd_version(): 21 | res = system.run_cmd('{} --version'.format(BASE_CMD), print_error=False) 22 | 23 | if not res: 24 | return None 25 | else: 26 | lines = res.split('\n') 27 | 28 | if lines and len(lines) >= 2: 29 | version = lines[1].split(' ')[-1].strip() 30 | return version.lower() if version else None 31 | else: 32 | return None 33 | 34 | 35 | def app_str_to_json(app: str) -> dict: 36 | app_data = [word for word in app.split(' ') if word] 37 | app_json = { 38 | 'name': app_data[0], 39 | 'version': app_data[1], 40 | 'rev': app_data[2], 41 | 'tracking': app_data[3], 42 | 'publisher': app_data[4] if len(app_data) >= 5 else None, 43 | 'notes': app_data[5] if len(app_data) >= 6 else None 44 | } 45 | 46 | app_json.update(get_info(app_json['name'], ('summary', 'type', 'description'))) 47 | return app_json 48 | 49 | 50 | def get_info(app_name: str, attrs: tuple = None): 51 | full_info_lines = system.run_cmd('{} info {}'.format(BASE_CMD, app_name)) 52 | 53 | data = {} 54 | 55 | if full_info_lines: 56 | re_attrs = r'\w+' if not attrs else '|'.join(attrs) 57 | info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines) 58 | 59 | for info in info_map: 60 | data[info[0]] = info[1].strip() 61 | 62 | if not attrs or 'description' in attrs: 63 | desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines) 64 | data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None 65 | 66 | if not attrs or 'commands' in attrs: 67 | commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines) 68 | data['commands'] = commands[0][0].replace('-', '').strip().split('\n') if commands else None 69 | 70 | return data 71 | 72 | 73 | def read_installed() -> List[dict]: 74 | res = system.run_cmd('{} list'.format(BASE_CMD), print_error=False) 75 | 76 | apps = [] 77 | 78 | if res and len(res) > 0: 79 | lines = res.split('\n') 80 | 81 | if not lines[0].startswith('error'): 82 | for idx, app_str in enumerate(lines): 83 | if idx > 0 and app_str: 84 | apps.append(app_str_to_json(app_str)) 85 | 86 | return apps 87 | 88 | 89 | def search(word: str, exact_name: bool = False) -> List[dict]: 90 | apps = [] 91 | 92 | res = system.run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False) 93 | 94 | if res: 95 | res = res.split('\n') 96 | 97 | if not res[0].startswith('No matching'): 98 | for idx, app_str in enumerate(res): 99 | if idx > 0 and app_str: 100 | app_data = [word for word in app_str.split(' ') if word] 101 | 102 | if exact_name and app_data[0] != word: 103 | continue 104 | 105 | apps.append({ 106 | 'name': app_data[0], 107 | 'version': app_data[1], 108 | 'publisher': app_data[2], 109 | 'notes': app_data[3] if app_data[3] != '-' else None, 110 | 'summary': app_data[4] if len(app_data) == 5 else '', 111 | 'rev': None, 112 | 'tracking': None, 113 | 'type': None 114 | }) 115 | 116 | if exact_name and len(apps) > 0: 117 | break 118 | 119 | return apps 120 | 121 | 122 | def uninstall_and_stream(app_name: str, root_password: str): 123 | return system.cmd_as_root([BASE_CMD, 'remove', app_name], root_password) 124 | 125 | 126 | def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen: 127 | 128 | install_cmd = [BASE_CMD, 'install', app_name] # default 129 | 130 | if confinement == 'classic': 131 | install_cmd.append('--classic') 132 | 133 | return system.cmd_as_root(install_cmd, root_password) 134 | 135 | 136 | def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen: 137 | return system.cmd_as_root([BASE_CMD, 'revert', app_name], root_password) 138 | 139 | 140 | def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen: 141 | return system.cmd_as_root([BASE_CMD, 'refresh', app_name], root_password) 142 | -------------------------------------------------------------------------------- /fpakman/core/snap/worker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import traceback 3 | 4 | from colorama import Fore 5 | 6 | from fpakman.core.controller import ApplicationManager 7 | from fpakman.core.model import ApplicationStatus 8 | from fpakman.core.snap import snap 9 | from fpakman.core.snap.constants import SNAP_API_URL 10 | from fpakman.core.snap.model import SnapApplication 11 | from fpakman.core.worker import AsyncDataLoader 12 | from fpakman.util.cache import Cache 13 | 14 | 15 | class SnapAsyncDataLoader(AsyncDataLoader): 16 | 17 | def __init__(self, app: SnapApplication, manager: ApplicationManager, http_session, api_cache: Cache, download_icons: bool, attempts: int = 2, timeout: int = 30): 18 | super(SnapAsyncDataLoader, self).__init__(app=app) 19 | self.manager = manager 20 | self.http_session = http_session 21 | self.attempts = attempts 22 | self.api_cache = api_cache 23 | self.timeout = timeout 24 | self.persist = False 25 | self.download_icons = download_icons 26 | 27 | def run(self): 28 | if self.app: 29 | self.app.status = ApplicationStatus.LOADING_DATA 30 | 31 | for _ in range(0, self.attempts): 32 | try: 33 | res = self.http_session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.base_data.name), timeout=self.timeout) 34 | 35 | if res.status_code == 200 and res.text: 36 | 37 | try: 38 | snap_list = res.json()['_embedded']['clickindex:package'] 39 | except: 40 | self.log_msg('Snap API response responded differently from expected for app: {}'.format(self.app.base_data.name)) 41 | break 42 | 43 | if not snap_list: 44 | break 45 | 46 | snap_data = snap_list[0] 47 | 48 | api_data = { 49 | 'confinement': snap_data.get('confinement'), 50 | 'description': snap_data.get('description'), 51 | 'icon_url': snap_data.get('icon_url') if self.download_icons else None 52 | } 53 | 54 | self.api_cache.add(self.app.base_data.id, api_data) 55 | self.app.confinement = api_data['confinement'] 56 | self.app.base_data.icon_url = api_data['icon_url'] 57 | 58 | if not api_data.get('description'): 59 | api_data['description'] = snap.get_info(self.app.base_data.name, ('description',)).get('description') 60 | 61 | self.app.base_data.description = api_data['description'] 62 | 63 | self.app.status = ApplicationStatus.READY 64 | self.persist = self.app.supports_disk_cache() 65 | break 66 | else: 67 | self.log_msg("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.base_data.id, res.status_code, res.content.decode()), Fore.RED) 68 | except: 69 | self.log_msg("Could not retrieve app data for id '{}'".format(self.app.base_data.id), Fore.YELLOW) 70 | traceback.print_exc() 71 | time.sleep(0.5) 72 | 73 | self.app.status = ApplicationStatus.READY 74 | 75 | if self.persist: 76 | self.manager.cache_to_disk(app=self.app, icon_bytes=None, only_icon=False) 77 | 78 | def clone(self) -> "SnapAsyncDataLoader": 79 | return SnapAsyncDataLoader(manager=self.manager, 80 | api_cache=self.api_cache, 81 | attempts=self.attempts, 82 | http_session=self.http_session, 83 | timeout=self.timeout, 84 | app=self.app) 85 | 86 | -------------------------------------------------------------------------------- /fpakman/core/system.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from typing import List 4 | 5 | from fpakman import __app_name__ 6 | from fpakman.core import resource 7 | 8 | 9 | class FpakmanProcess: 10 | 11 | def __init__(self, subproc: subprocess.Popen, success_phrase: str = None, wrong_error_phrase: str = '[sudo] password for'): 12 | self.subproc = subproc 13 | self.success_pgrase = success_phrase 14 | self.wrong_error_phrase = wrong_error_phrase 15 | 16 | 17 | def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True) -> str: 18 | args = { 19 | "shell": True, 20 | "stdout": subprocess.PIPE, 21 | "env": {'LANG': 'en'} 22 | } 23 | 24 | if not print_error: 25 | args["stderr"] = subprocess.DEVNULL 26 | 27 | res = subprocess.run(cmd, **args) 28 | return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None 29 | 30 | 31 | def stream_cmd(cmd: List[str]): 32 | return cmd_to_subprocess(cmd).stdout 33 | 34 | 35 | def cmd_to_subprocess(cmd: List[str]): 36 | return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'LANG': 'en'}) 37 | 38 | 39 | def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')): 40 | os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_path) if icon_path else '', msg)) 41 | 42 | 43 | def cmd_as_root(cmd: List[str], root_password: str) -> subprocess.Popen: 44 | pwdin, final_cmd = None, [] 45 | 46 | if root_password is not None: 47 | final_cmd.extend(['sudo', '-S']) 48 | pwdin = stream_cmd(['echo', root_password]) 49 | 50 | final_cmd.extend(cmd) 51 | return subprocess.Popen(final_cmd, stdin=pwdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 52 | -------------------------------------------------------------------------------- /fpakman/core/worker.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | from threading import Thread 3 | 4 | from colorama import Fore 5 | 6 | from fpakman.core.model import Application 7 | 8 | 9 | class AsyncDataLoader(Thread): 10 | 11 | def __init__(self, app: Application): 12 | super(AsyncDataLoader, self).__init__(daemon=True) 13 | self.id_ = '{}#{}'.format(self.__class__.__name__, id(self)) 14 | self.app = app 15 | 16 | def log_msg(self, msg: str, color: int = None): 17 | final_msg = StringIO() 18 | 19 | if color: 20 | final_msg.write(str(color)) 21 | 22 | final_msg.write('[{}] '.format(self.id_)) 23 | 24 | final_msg.write(msg) 25 | 26 | if color: 27 | final_msg.write(Fore.RESET) 28 | 29 | final_msg.seek(0) 30 | 31 | print(final_msg.read()) 32 | -------------------------------------------------------------------------------- /fpakman/resources/img/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 35 | 36 | 69 | 70 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 89 | 90 | 93 | 94 | 97 | 98 | 101 | 102 | 105 | 106 | 109 | 110 | 113 | 114 | 117 | 118 | 121 | 122 | 125 | 126 | 129 | 130 | -------------------------------------------------------------------------------- /fpakman/resources/img/app_info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 50 | 54 | 60 | 65 | 66 | 69 | 70 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 89 | 90 | 93 | 94 | 97 | 98 | 101 | 102 | 105 | 106 | 109 | 110 | 113 | 114 | 117 | 118 | 121 | 122 | 125 | 126 | -------------------------------------------------------------------------------- /fpakman/resources/img/app_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 58 | 63 | 64 | -------------------------------------------------------------------------------- /fpakman/resources/img/app_update.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 61 | 66 | 71 | 72 | 76 | 77 | -------------------------------------------------------------------------------- /fpakman/resources/img/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 62 | 63 | -------------------------------------------------------------------------------- /fpakman/resources/img/downgrade.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 61 | 66 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /fpakman/resources/img/exclamation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 75 | 76 | 78 | 79 | 81 | 82 | 84 | 85 | 87 | 88 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 102 | 103 | 105 | 106 | 108 | 109 | 111 | 112 | 114 | 115 | 117 | 118 | 120 | 121 | 127 | 132 | -------------------------------------------------------------------------------- /fpakman/resources/img/flathub.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 66 | 72 | 76 | 86 | 91 | 96 | 106 | 107 | 111 | 121 | 131 | 136 | 141 | 142 | 146 | 156 | 166 | 171 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /fpakman/resources/img/install.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 50 | 54 | 59 | 60 | 63 | 64 | 67 | 68 | 71 | 72 | 75 | 76 | 79 | 80 | 83 | 84 | 87 | 88 | 91 | 92 | 95 | 96 | 99 | 100 | 103 | 104 | 107 | 108 | 111 | 112 | 115 | 116 | 119 | 120 | -------------------------------------------------------------------------------- /fpakman/resources/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/resources/img/logo.png -------------------------------------------------------------------------------- /fpakman/resources/img/logo_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/resources/img/logo_update.png -------------------------------------------------------------------------------- /fpakman/resources/img/red_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 50 | 55 | 58 | 59 | 62 | 63 | 66 | 67 | 70 | 71 | 74 | 75 | 78 | 79 | 82 | 83 | 86 | 87 | 90 | 91 | 94 | 95 | 98 | 99 | 102 | 103 | 106 | 107 | 110 | 111 | 114 | 115 | -------------------------------------------------------------------------------- /fpakman/resources/img/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 67 | 71 | 74 | 79 | 84 | 85 | 86 | 89 | 90 | 93 | 94 | 97 | 98 | 101 | 102 | 105 | 106 | 109 | 110 | 113 | 114 | 117 | 118 | 121 | 122 | 125 | 126 | 129 | 130 | 133 | 134 | 137 | 138 | 141 | 142 | 145 | 146 | -------------------------------------------------------------------------------- /fpakman/resources/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 50 | 54 | 59 | 60 | 63 | 64 | 67 | 68 | 71 | 72 | 75 | 76 | 79 | 80 | 83 | 84 | 87 | 88 | 91 | 92 | 95 | 96 | 99 | 100 | 103 | 104 | 107 | 108 | 111 | 112 | 115 | 116 | 119 | 120 | -------------------------------------------------------------------------------- /fpakman/resources/img/snapcraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/resources/img/snapcraft.png -------------------------------------------------------------------------------- /fpakman/resources/img/uninstall.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 57 | 60 | 63 | 67 | 71 | 75 | 79 | 80 | 85 | 90 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /fpakman/resources/img/update_green.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 40 | 44 | 45 | 54 | 63 | 72 | 73 | 98 | 102 | 107 | 112 | 113 | 117 | 118 | -------------------------------------------------------------------------------- /fpakman/resources/locale/en: -------------------------------------------------------------------------------- 1 | manage_window.title=Applications Manager 2 | manage_window.columns.latest_version=Latest Version 3 | manage_window.columns.update=Upgrade ? 4 | manage_window.apps_table.row.actions.info=Information 5 | manage_window.apps_table.row.actions.history=History 6 | manage_window.apps_table.row.actions.uninstall.popup.title=Uninstall 7 | manage_window.apps_table.row.actions.uninstall.popup.body=Remove {} from your computer ? 8 | manage_window.apps_table.row.actions.install.popup.title=Installation 9 | manage_window.apps_table.row.actions.install.popup.body=Install {} on your computer ? 10 | manage_window.apps_table.row.actions.downgrade=Downgrade 11 | manage_window.apps_table.row.actions.downgrade.popup.body=Do you really want to downgrade {} ? 12 | manage_window.apps_table.row.actions.install=Install 13 | manage_window.apps_table.row.actions.refresh=Refresh 14 | manage_window.apps_table.upgrade_toggle.tooltip=There is an update for this application. Click here to check or uncheck the update 15 | manage_window.checkbox.only_apps=Apps 16 | window_manage.input_search.placeholder=Search 17 | window_manage.input_search.tooltip=Type and press ENTER to search for applications 18 | manage_window.label.updates=Updates 19 | manage_window.status.refreshing=Refreshing 20 | manage_window.status.upgrading=Upgrading 21 | manage_window.status.uninstalling=Uninstalling 22 | manage_window.status.downgrading=Downgrading 23 | manage_window.status.info=Retrieving information 24 | manage_window.status.history=Retrieving history 25 | manage_window.status.searching=Searching 26 | manage_window.status.installing=Installing 27 | manage_window.status.refreshing_app=Refreshing 28 | manage_window.bt.refresh.tooltip=Reload the data about installed applications 29 | manage_window.bt.upgrade.tooltip=Upgrade all checked applications 30 | manage_window.checkbox.show_details=Show details 31 | popup.root.title=Requires root privileges 32 | popup.root.password=Password 33 | popup.root.bad_password.title=Error 34 | popup.root.bad_password.body=Wrong password 35 | popup.downgrade.impossible.title=Error 36 | popup.downgrade.impossible.body=Impossible to downgrade: the app is in its first version 37 | popup.history.title=History 38 | popup.history.selected.tooltip=Current version 39 | popup.button.yes=Yes 40 | popup.button.no=No 41 | popup.button.cancel=Cancel 42 | tray.action.manage=Manage applications 43 | tray.action.exit=Exit 44 | tray.action.about=About 45 | tray.action.refreshing=Refreshing 46 | notification.new_updates=Updates 47 | notification.update_selected.success=app(s) updated successfully 48 | notification.update_selected.failed=Update failed 49 | notification.install.failed=installation failed 50 | notification.uninstall.failed=uninstallation failed 51 | notification.downgrade.failed=Failed to downgrade 52 | flatpak.info.arch=arch 53 | flatpak.info.branch=branch 54 | flatpak.info.collection=collection 55 | flatpak.info.commit=commit 56 | flatpak.info.date=date 57 | flatpak.info.description=description 58 | flatpak.info.id=id 59 | flatpak.info.installation=installation 60 | flatpak.info.installed=installed 61 | flatpak.info.license=license 62 | flatpak.info.name=name 63 | flatpak.info.origin=origin 64 | flatpak.info.parent=parent 65 | flatpak.info.ref=ref 66 | flatpak.info.runtime=runtime 67 | flatpak.info.sdk=sdk 68 | flatpak.info.subject=subject 69 | flatpak.info.type=type 70 | flatpak.info.version=version 71 | snap.info.commands=commands 72 | snap.info.contact=contact 73 | snap.info.description=description 74 | snap.info.license=license 75 | snap.info.revision=revision 76 | snap.info.tracking=tracking 77 | snap.info.installed=installed 78 | snap.info.publisher=publisher 79 | about.info.desc=Non-official Flatpak / Snap application management graphical interface 80 | about.info.link=More information at 81 | about.info.license=Free license 82 | about.info.rate=If this tool is useful for you, give it a star on Github to keep it up 83 | yes=yes 84 | no=no 85 | version.updated=updated 86 | version.outdated=outdated 87 | name=name 88 | version=version 89 | latest_version=latest version 90 | description=description 91 | type=type 92 | installed=installed 93 | uninstalled=uninstalled 94 | not_installed=not installed 95 | downgraded=downgraded 96 | others=others 97 | internet.required=Internet connection is required 98 | license.unset=unset 99 | updates=updates 100 | version.installed=installed version 101 | version.latest=latest version 102 | version.installed_outdated=the installed version is outdated 103 | version.unknown=not informed 104 | app.name=application name 105 | warning=warning 106 | snap.notification.snapd_unavailable=snapd seems not to be enabled. snap packages will not be available. 107 | flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. 108 | install=install 109 | uninstall=uninstall 110 | bt.app_upgrade=Upgrade 111 | bt.app_not_upgrade=Don't upgrade 112 | manage_window.upgrade_all.popup.title=Upgrade 113 | manage_window.upgrade_all.popup.body=Upgrade all selected applications ? 114 | manage_window.bt_about.tooltip=About -------------------------------------------------------------------------------- /fpakman/resources/locale/es: -------------------------------------------------------------------------------- 1 | manage_window.title=Administrador de Aplicativos 2 | manage_window.columns.latest_version=Ultima Versión 3 | manage_window.columns.update=Actualizar ? 4 | manage_window.columns.installed=Instalado 5 | manage_window.apps_table.row.actions.info=Información 6 | manage_window.apps_table.row.actions.history=Historia 7 | manage_window.apps_table.row.actions.uninstall=Desinstalar 8 | manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalación 9 | manage_window.apps_table.row.actions.uninstall.popup.body=¿Eliminar {} de tu ordenador? 10 | manage_window.apps_table.row.actions.install.popup.title=Instalación 11 | manage_window.apps_table.row.actions.install.popup.body=¿Instalar {} en tu ordenador? 12 | manage_window.apps_table.row.actions.downgrade=Revertir versión 13 | manage_window.apps_table.row.actions.downgrade.popup.body=¿Realmente quieres revertir la versión actual de {}? 14 | manage_window.apps_table.row.actions.install=Instalar 15 | manage_window.apps_table.row.actions.refresh=Actualizar 16 | manage_window.apps_table.upgrade_toggle.tooltip=Haces clic aqui para marcar o desmarcar la actualización 17 | manage_window.checkbox.only_apps=Aplicativos 18 | window_manage.input_search.placeholder=Buscar 19 | window_manage.input_search.tooltip=Escriba y oprima ENTER para buscar aplicativos 20 | manage_window.label.updates=Actualizaciones 21 | manage_window.status.refreshing=Recargando 22 | manage_window.status.upgrading=Actualizando 23 | manage_window.status.uninstalling=Desinstalando 24 | manage_window.status.downgrading=Revirtiendo 25 | manage_window.status.info=Obteniendo información 26 | manage_window.status.history=Obteniendo la historia 27 | manage_window.status.searching=Buscando 28 | manage_window.status.installing=Instalando 29 | manage_window.status.refreshing_app=Actualizando 30 | manage_window.bt.refresh.tooltip=Recarga los datos sobre los aplicativos instalados 31 | manage_window.bt.upgrade.tooltip=Actualiza todos los aplicativos marcados 32 | manage_window.checkbox.show_details=Mostrar detalles 33 | popup.root.title=Requiere privilegios de root 34 | popup.root.password=Contraseña 35 | popup.root.bad_password.title=Error 36 | popup.root.bad_password.body=Contraseña incorrecta 37 | popup.downgrade.impossible.title=Error 38 | popup.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión 39 | popup.history.title=Historia 40 | popup.history.selected.tooltip=Versión actual 41 | popup.button.yes=Sí 42 | popup.button.no=No 43 | popup.button.cancel=Cancelar 44 | tray.action.manage=Administrar aplicativos 45 | tray.action.exit=Salir 46 | tray.action.about=Sobre 47 | tray.action.refreshing=Recargando 48 | notification.new_updates=Actualizaciones 49 | notification.update_selected.success=aplicativo(s) actualizado(s) con éxito 50 | notification.update_selected.failed=La actualización falló 51 | notification.install.failed=la instalación ha fallado 52 | notification.uninstall.failed=la desinstalación ha fallado 53 | notification.downgrade.failed=Error al revertir la versión 54 | flatpak.info.arch=arquitectura 55 | flatpak.info.branch=rama 56 | flatpak.info.collection=colección 57 | flatpak.info.commit=commit 58 | flatpak.info.date=fecha 59 | flatpak.info.description=descripción 60 | flatpak.info.id=id 61 | flatpak.info.installation=instalación 62 | flatpak.info.installed=instalado 63 | flatpak.info.license=licencia 64 | flatpak.info.name=nombre 65 | flatpak.info.origin=origen 66 | flatpak.info.parent=padre 67 | flatpak.info.ref=ref 68 | flatpak.info.runtime=runtime 69 | flatpak.info.sdk=sdk 70 | flatpak.info.subject=tema 71 | flatpak.info.type=tipo 72 | flatpak.info.version=versión 73 | snap.info.commands=comandos 74 | snap.info.contact=contacto 75 | snap.info.description=descripción 76 | snap.info.license=licencia 77 | snap.info.revision=revisión 78 | snap.info.tracking=tracking 79 | snap.info.installed=instalado 80 | snap.info.publisher=publicador 81 | about.info.desc=Interfaz grafica no oficial para administración de aplicativos Flatpak / Snap 82 | about.info.link=Mas informaciones en 83 | about.info.license=Licencia gratuita 84 | about.info.rate=Si esta herramienta es útil para ti, dale una estrella en Github para mantenerla 85 | yes=sí 86 | no=no 87 | version.updated=actualizada 88 | version.outdated=desactualizada 89 | name=nombre 90 | version=versión 91 | latest_version=ultima versión 92 | description=descripción 93 | type=tipo 94 | installed=instalado 95 | uninstalled=desinstalado 96 | not_installed=no instalado 97 | downgraded=versión revertida 98 | others=otros 99 | internet.required=Se requiere conexión a internet 100 | license.unset=No está definida 101 | updates=actualizaciones 102 | version.installed=versión instalada 103 | version.latest=versión más reciente 104 | version.installed_outdated=la versión instalada está desactualizada 105 | version.unknown=versión no informada 106 | app.name=nombre del aplicativo 107 | warning=aviso 108 | snap.notification.snapd_unavailable=snapd no parece estar habilitado. los paquetes snap estarán indisponibles. 109 | flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak. 110 | install=instalar 111 | uninstall=desinstalar 112 | bt.app_upgrade=Actualizar 113 | bt.app_not_upgrade=No actualizar 114 | manage_window.upgrade_all.popup.title=Actualizar 115 | manage_window.upgrade_all.popup.body=¿Actualizar todos los aplicativos seleccionados? 116 | manage_window.bt_about.tooltip=Sobre -------------------------------------------------------------------------------- /fpakman/resources/locale/pt: -------------------------------------------------------------------------------- 1 | manage_window.title=Gerenciador de Aplicativos 2 | manage_window.columns.latest_version=Última Versão 3 | manage_window.columns.update=Atualizar ? 4 | manage_window.columns.installed=Instalado 5 | manage_window.apps_table.row.actions.info=Informação 6 | manage_window.apps_table.row.actions.history=Histórico 7 | manage_window.apps_table.row.actions.uninstall=Desinstalar 8 | manage_window.apps_table.row.actions.uninstall.popup.title=Desinstalação 9 | manage_window.apps_table.row.actions.uninstall.popup.body=Remover {} do seu computador ? 10 | manage_window.apps_table.row.actions.install.popup.title=Instalação 11 | manage_window.apps_table.row.actions.install.popup.body=Instalar {} no seu computador ? 12 | manage_window.apps_table.row.actions.downgrade=Reverter versão 13 | manage_window.apps_table.row.actions.downgrade.popup.body=Você realmente quer reverter a versão atual de {} ? 14 | manage_window.apps_table.row.actions.install=Instalar 15 | manage_window.apps_table.row.actions.refresh=Atualizar 16 | manage_window.apps_table.upgrade_toggle.tooltip=Existe um atualização para esse aplicativo. Clique aqui para marcar ou desmarcar a atualização 17 | manage_window.checkbox.only_apps=Aplicativos 18 | window_manage.input_search.placeholder=Buscar 19 | window_manage.input_search.tooltip=Digite e pressione ENTER para buscar aplicativos 20 | manage_window.label.updates=Atualizações 21 | manage_window.status.refreshing=Recarregando 22 | manage_window.status.upgrading=Atualizando 23 | manage_window.status.uninstalling=Desinstalando 24 | manage_window.status.downgrading=Revertendo 25 | manage_window.status.info=Obtendo informação 26 | manage_window.status.history=Obtendo histórico 27 | manage_window.status.searching=Buscando 28 | manage_window.status.installing=Instalando 29 | manage_window.status.refreshing_app=Atualizando 30 | manage_window.bt.refresh.tooltip=Recarrega os dados sobre os aplicativos instalados 31 | manage_window.bt.upgrade.tooltip=Atualiza todos os aplicativos marcados 32 | manage_window.checkbox.show_details=Mostrar detalhes 33 | popup.root.title=Requer privilégios de root 34 | popup.root.password=Senha 35 | popup.root.bad_password.title=Erro 36 | popup.root.bad_password.body=Senha incorreta 37 | popup.downgrade.impossible.title=Erro 38 | popup.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão 39 | popup.history.title=Histórico 40 | popup.history.selected.tooltip=Versão atual 41 | popup.button.yes=Sim 42 | popup.button.no=Não 43 | popup.button.cancel=Cancelar 44 | tray.action.manage=Gerenciar aplicativos 45 | tray.action.exit=Sair 46 | tray.action.about=Sobre 47 | tray.action.refreshing=Recarregando 48 | notification.new_updates=Atualizações 49 | notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso 50 | notification.update_selected.failed=Erro ao atualizar 51 | notification.install.failed=instalação falhou 52 | notification.uninstall.failed=desinstalação falhou 53 | notification.downgrade.failed=Erro ao reverter versão 54 | flatpak.info.arch=arquitetura 55 | flatpak.info.branch=ramo 56 | flatpak.info.collection=coleção 57 | flatpak.info.commit=commit 58 | flatpak.info.date=data 59 | flatpak.info.description=descrição 60 | flatpak.info.id=id 61 | flatpak.info.installation=instalação 62 | flatpak.info.installed=instalado 63 | flatpak.info.license=licença 64 | flatpak.info.name=nome 65 | flatpak.info.origin=origem 66 | flatpak.info.parent=pai 67 | flatpak.info.ref=ref 68 | flatpak.info.runtime=runtime 69 | flatpak.info.sdk=sdk 70 | flatpak.info.subject=assunto 71 | flatpak.info.type=tipo 72 | flatpak.info.version=versão 73 | snap.info.commands=comandos 74 | snap.info.contact=contato 75 | snap.info.description=descrição 76 | snap.info.license=licença 77 | snap.info.revision=revisão 78 | snap.info.tracking=tracking 79 | snap.info.installed=instalado 80 | snap.info.publisher=publicador 81 | about.info.desc=Interface gráfica não oficial para gerenciamento de aplicativos Flatpak / Snap 82 | about.info.link=Mais informações em 83 | about.info.license=Licença gratuita 84 | about.info.rate=Se essa ferramenta é útil para você, dê uma estrela no Github para mantê-la de pé 85 | yes=sim 86 | no=não 87 | version.updated=atualizada 88 | version.outdated=desatualizada 89 | name=nome 90 | version=versão 91 | latest_version=última versão 92 | description=descrição 93 | type=tipo 94 | installed=instalado 95 | uninstalled=desinstalado 96 | not_installed=não instalado 97 | downgraded=versão revertida 98 | others=outros 99 | internet.required=É necessário estar conectado a Internet 100 | license.unset=Não definida 101 | updates=atualizações 102 | version.installed=versão instalada 103 | version.latest=versão mais recente 104 | version.installed_outdated=a versão instalada está desatualizada 105 | version.unknown=versão não informada 106 | app.name=nome do aplicativo 107 | warning=aviso 108 | snap.notification.snapd_unavailable=snapd não parece estar habilitado. os pacotes snap estarão indisponíveis. 109 | flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak. 110 | install=instalar 111 | uninstall=desinstalar 112 | bt.app_upgrade=Atualizar 113 | bt.app_not_upgrade=Não atualizar 114 | manage_window.upgrade_all.popup.title=Atualizar 115 | manage_window.upgrade_all.popup.body=Atualizar todos os aplicativos selecionados ? 116 | manage_window.bt_about.tooltip=Sobre -------------------------------------------------------------------------------- /fpakman/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/util/__init__.py -------------------------------------------------------------------------------- /fpakman/util/cache.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from threading import Lock 3 | 4 | 5 | class Cache: 6 | 7 | def __init__(self, expiration_time: int): 8 | self.expiration_time = expiration_time 9 | self._cache = {} 10 | self.lock = Lock() 11 | 12 | def is_enabled(self): 13 | return self.expiration_time < 0 or self.expiration_time > 0 14 | 15 | def add(self, key: str, val: object): 16 | if key and self.is_enabled(): 17 | self.lock.acquire() 18 | self._add(key, val) 19 | self.lock.release() 20 | 21 | def _add(self, key: str, val: object): 22 | if key: 23 | self._cache[key] = {'val': val, 'expires_at': datetime.utcnow() + timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None} 24 | 25 | def add_non_existing(self, key: str, val: object): 26 | 27 | if key and self. is_enabled(): 28 | self.lock.acquire() 29 | cur_val = self.get(key) 30 | 31 | if cur_val is None: 32 | self._add(key, val) 33 | 34 | self.lock.release() 35 | 36 | def get(self, key: str): 37 | if key and self.is_enabled(): 38 | val = self._cache.get(key) 39 | 40 | if val: 41 | expiration = val.get('expires_at') 42 | 43 | if expiration and expiration <= datetime.utcnow(): 44 | self.lock.acquire() 45 | del self._cache[key] 46 | self.lock.release() 47 | return None 48 | 49 | return val['val'] 50 | 51 | def delete(self, key): 52 | if key and self.is_enabled(): 53 | if key in self._cache: 54 | self.lock.acquire() 55 | del self._cache[key] 56 | self.lock.release() 57 | 58 | def keys(self): 59 | return set(self._cache.keys()) if self.is_enabled() else set() 60 | 61 | def clean_expired(self): 62 | if self.is_enabled(): 63 | for key in self.keys(): 64 | self.get(key) 65 | -------------------------------------------------------------------------------- /fpakman/util/memory.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread 3 | from typing import List 4 | 5 | from fpakman.util.cache import Cache 6 | 7 | 8 | class CacheCleaner(Thread): 9 | 10 | def __init__(self, caches: List[Cache], check_interval: int = 15): 11 | super(CacheCleaner, self).__init__(daemon=True) 12 | self.caches = [c for c in caches if c.is_enabled()] 13 | self.check_interval = check_interval 14 | 15 | def run(self): 16 | 17 | if self.caches: 18 | while True: 19 | for cache in self.caches: 20 | cache.clean_expired() 21 | 22 | time.sleep(self.check_interval) 23 | 24 | -------------------------------------------------------------------------------- /fpakman/util/util.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | from fpakman.core import resource 4 | import glob 5 | import re 6 | 7 | HTML_RE = re.compile(r'<[^>]+>') 8 | 9 | 10 | def get_locale_keys(key: str = None): 11 | 12 | locale_path = None 13 | 14 | if key is None: 15 | current_locale = locale.getdefaultlocale() 16 | else: 17 | current_locale = [key.strip().lower()] 18 | 19 | if current_locale: 20 | current_locale = current_locale[0] 21 | 22 | locale_dir = resource.get_path('locale') 23 | 24 | for locale_file in glob.glob(locale_dir + '/*'): 25 | name = locale_file.split('/')[-1] 26 | 27 | if current_locale == name or current_locale.startswith(name + '_'): 28 | locale_path = locale_file 29 | break 30 | 31 | if not locale_path: 32 | locale_path = resource.get_path('locale/en') 33 | 34 | with open(locale_path, 'r') as f: 35 | locale_keys = f.readlines() 36 | 37 | locale_obj = {} 38 | for line in locale_keys: 39 | if line: 40 | keyval = line.strip().split('=') 41 | locale_obj[keyval[0].strip()] = keyval[1].strip() 42 | 43 | return locale_obj 44 | 45 | 46 | def strip_html(string: str): 47 | return HTML_RE.sub('', string) 48 | -------------------------------------------------------------------------------- /fpakman/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/view/__init__.py -------------------------------------------------------------------------------- /fpakman/view/qt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/view/qt/__init__.py -------------------------------------------------------------------------------- /fpakman/view/qt/about.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtGui import QPixmap 3 | from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel 4 | 5 | from fpakman import __version__, __app_name__ 6 | from fpakman.core import resource 7 | 8 | PROJECT_URL = 'https://github.com/vinifmor/fpakman' 9 | LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/fpakman/master/LICENSE' 10 | 11 | 12 | class AboutDialog(QDialog): 13 | 14 | def __init__(self, locale_keys: dict): 15 | super(AboutDialog, self).__init__() 16 | self.setWindowTitle(locale_keys['tray.action.about']) 17 | layout = QVBoxLayout() 18 | self.setLayout(layout) 19 | 20 | label_logo = QLabel(self) 21 | label_logo.setPixmap(QPixmap(resource.get_path('img/logo.svg'))) 22 | label_logo.setAlignment(Qt.AlignCenter) 23 | layout.addWidget(label_logo) 24 | 25 | label_name = QLabel('{} ( {} {} )'.format(__app_name__, locale_keys['flatpak.info.version'].lower(), __version__)) 26 | label_name.setStyleSheet('font-weight: bold;') 27 | label_name.setAlignment(Qt.AlignCenter) 28 | layout.addWidget(label_name) 29 | 30 | layout.addWidget(QLabel('')) 31 | 32 | line_desc = QLabel(self) 33 | line_desc.setStyleSheet('font-size: 10px; font-weight: bold;') 34 | line_desc.setText(locale_keys['about.info.desc']) 35 | line_desc.setAlignment(Qt.AlignCenter) 36 | line_desc.setMinimumWidth(400) 37 | layout.addWidget(line_desc) 38 | 39 | layout.addWidget(QLabel('')) 40 | 41 | label_more_info = QLabel() 42 | label_more_info.setStyleSheet('font-size: 9px;') 43 | label_more_info.setText(locale_keys['about.info.link'] + ": {url}".format(url=PROJECT_URL)) 44 | label_more_info.setOpenExternalLinks(True) 45 | label_more_info.setAlignment(Qt.AlignCenter) 46 | layout.addWidget(label_more_info) 47 | 48 | label_license = QLabel() 49 | label_license.setStyleSheet('font-size: 9px;') 50 | label_license.setText("{}".format(LICENSE_URL, locale_keys['about.info.license'])) 51 | label_license.setOpenExternalLinks(True) 52 | label_license.setAlignment(Qt.AlignCenter) 53 | layout.addWidget(label_license) 54 | 55 | layout.addWidget(QLabel('')) 56 | 57 | label_rate = QLabel() 58 | label_rate.setStyleSheet('font-size: 9px; font-weight: bold;') 59 | label_rate.setText(locale_keys['about.info.rate'] + ' :)') 60 | label_rate.setOpenExternalLinks(True) 61 | label_rate.setAlignment(Qt.AlignCenter) 62 | layout.addWidget(label_rate) 63 | 64 | layout.addWidget(QLabel('')) 65 | 66 | self.adjustSize() 67 | self.setFixedSize(self.size()) 68 | 69 | def closeEvent(self, event): 70 | event.ignore() 71 | self.hide() 72 | -------------------------------------------------------------------------------- /fpakman/view/qt/dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import QIcon 2 | from PyQt5.QtWidgets import QMessageBox 3 | 4 | from fpakman.core import resource 5 | 6 | 7 | def show_error(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))): 8 | error_msg = QMessageBox() 9 | error_msg.setIcon(QMessageBox.Critical) 10 | error_msg.setWindowTitle(title) 11 | error_msg.setText(body) 12 | 13 | if icon: 14 | error_msg.setWindowIcon(icon) 15 | 16 | error_msg.exec_() 17 | 18 | 19 | def show_warning(title: str, body: str, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))): 20 | msg = QMessageBox() 21 | msg.setIcon(QMessageBox.Warning) 22 | msg.setWindowTitle(title) 23 | msg.setText(body) 24 | 25 | if icon: 26 | msg.setWindowIcon(icon) 27 | 28 | msg.exec_() 29 | 30 | 31 | def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIcon(resource.get_path('img/logo.svg'))): 32 | dialog_confirmation = QMessageBox() 33 | dialog_confirmation.setIcon(QMessageBox.Question) 34 | dialog_confirmation.setWindowTitle(title) 35 | dialog_confirmation.setText(body) 36 | dialog_confirmation.setStyleSheet('QLabel { margin-right: 25px; }') 37 | 38 | bt_yes = dialog_confirmation.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole) 39 | dialog_confirmation.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole) 40 | 41 | if icon: 42 | dialog_confirmation.setWindowIcon(icon) 43 | 44 | dialog_confirmation.exec_() 45 | 46 | return dialog_confirmation.clickedButton() == bt_yes 47 | -------------------------------------------------------------------------------- /fpakman/view/qt/history.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from functools import reduce 3 | 4 | from PyQt5.QtCore import Qt 5 | from PyQt5.QtGui import QColor 6 | from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView 7 | 8 | from fpakman.util.cache import Cache 9 | 10 | 11 | class HistoryDialog(QDialog): 12 | 13 | def __init__(self, app: dict, icon_cache: Cache, locale_keys: dict): 14 | super(HistoryDialog, self).__init__() 15 | 16 | self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], app['model'].base_data.name)) 17 | 18 | layout = QVBoxLayout() 19 | self.setLayout(layout) 20 | 21 | table_history = QTableWidget() 22 | table_history.setFocusPolicy(Qt.NoFocus) 23 | table_history.setShowGrid(False) 24 | table_history.verticalHeader().setVisible(False) 25 | table_history.setAlternatingRowColors(True) 26 | 27 | table_history.setColumnCount(len(app['history'][0])) 28 | table_history.setRowCount(len(app['history'])) 29 | table_history.setHorizontalHeaderLabels([locale_keys['flatpak.info.' + key].capitalize() for key in sorted(app['history'][0].keys())]) 30 | 31 | for row, commit in enumerate(app['history']): 32 | 33 | current_app_commit = app['model'].commit == commit['commit'] 34 | 35 | for col, key in enumerate(sorted(commit.keys())): 36 | item = QTableWidgetItem() 37 | item.setText(commit[key]) 38 | item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 39 | 40 | if current_app_commit: 41 | item.setBackground(QColor('#ffbf00' if row != 0 else '#32CD32')) 42 | tip = '{}. {}.'.format(locale_keys['popup.history.selected.tooltip'], locale_keys['version.{}'.format('updated'if row == 0 else 'outdated')].capitalize()) 43 | 44 | item.setToolTip(tip) 45 | 46 | table_history.setItem(row, col, item) 47 | 48 | layout.addWidget(table_history) 49 | 50 | header_horizontal = table_history.horizontalHeader() 51 | for i in range(0, table_history.columnCount()): 52 | header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch) 53 | 54 | new_width = reduce(operator.add, [table_history.columnWidth(i) for i in range(table_history.columnCount())]) 55 | self.resize(new_width, table_history.height()) 56 | 57 | icon_data = icon_cache.get(app['model'].base_data.icon_url) 58 | 59 | if icon_data and icon_data.get('icon'): 60 | self.setWindowIcon(icon_data.get('icon')) 61 | -------------------------------------------------------------------------------- /fpakman/view/qt/info.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QSize 2 | from PyQt5.QtWidgets import QDialog, QVBoxLayout, QFormLayout, QGroupBox, \ 3 | QLineEdit, QLabel 4 | 5 | from fpakman.util import util 6 | from fpakman.util.cache import Cache 7 | 8 | IGNORED_ATTRS = {'name', '__app__'} 9 | 10 | 11 | class InfoDialog(QDialog): 12 | 13 | def __init__(self, app: dict, icon_cache: Cache, locale_keys: dict, screen_size: QSize()): 14 | super(InfoDialog, self).__init__() 15 | self.setWindowTitle(app['name']) 16 | self.screen_size = screen_size 17 | layout = QVBoxLayout() 18 | self.setLayout(layout) 19 | 20 | gbox_info = QGroupBox() 21 | gbox_info.setMaximumHeight(self.screen_size.height() - self.screen_size.height() * 0.1) 22 | gbox_info_layout = QFormLayout() 23 | gbox_info.setLayout(gbox_info_layout) 24 | 25 | layout.addWidget(gbox_info) 26 | 27 | icon_data = icon_cache.get(app['__app__'].model.base_data.icon_url) 28 | 29 | if icon_data and icon_data.get('icon'): 30 | self.setWindowIcon(icon_data.get('icon')) 31 | 32 | for attr in sorted(app.keys()): 33 | if attr not in IGNORED_ATTRS and app[attr]: 34 | val = app[attr] 35 | text = QLineEdit() 36 | text.setToolTip(val) 37 | 38 | if attr == 'license' and val.strip() == 'unset': 39 | val = locale_keys['license.unset'] 40 | 41 | if attr == 'description': 42 | val = util.strip_html(val) 43 | val = val[0:40] + '...' 44 | 45 | text.setText(val) 46 | text.setCursorPosition(0) 47 | text.setStyleSheet("width: 400px") 48 | text.setReadOnly(True) 49 | 50 | label = QLabel("{}: ".format(locale_keys.get(app['__app__'].model.get_type() + '.info.' + attr, attr)).capitalize()) 51 | label.setStyleSheet("font-weight: bold") 52 | 53 | gbox_info_layout.addRow(label, text) 54 | 55 | self.adjustSize() 56 | -------------------------------------------------------------------------------- /fpakman/view/qt/root.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import subprocess 4 | 5 | from PyQt5.QtWidgets import QInputDialog, QLineEdit 6 | 7 | from fpakman.view.qt.dialog import show_error 8 | 9 | 10 | def is_root(): 11 | return os.getuid() == 0 12 | 13 | 14 | def ask_root_password(locale_keys: dict): 15 | 16 | dialog_pwd = QInputDialog() 17 | dialog_pwd.setInputMode(QInputDialog.TextInput) 18 | dialog_pwd.setTextEchoMode(QLineEdit.Password) 19 | dialog_pwd.setWindowTitle(locale_keys['popup.root.title']) 20 | dialog_pwd.setLabelText(locale_keys['popup.root.password'] + ':') 21 | dialog_pwd.setCancelButtonText(locale_keys['popup.button.cancel']) 22 | dialog_pwd.resize(400, 200) 23 | 24 | ok = dialog_pwd.exec_() 25 | 26 | if ok: 27 | if not validate_password(dialog_pwd.textValue()): 28 | show_error(title=locale_keys['popup.root.bad_password.title'], 29 | body=locale_keys['popup.root.bad_password.body']) 30 | ok = False 31 | 32 | return dialog_pwd.textValue(), ok 33 | 34 | 35 | def validate_password(password: str) -> bool: 36 | proc = subprocess.Popen('sudo -k && echo {} | sudo -S whoami'.format(password), 37 | shell=True, 38 | stdout=subprocess.PIPE, 39 | stderr=subprocess.DEVNULL, 40 | bufsize=-1) 41 | stream = os._wrap_close(io.TextIOWrapper(proc.stdout), proc) 42 | res = stream.read() 43 | stream.close() 44 | 45 | return bool(res.strip()) 46 | -------------------------------------------------------------------------------- /fpakman/view/qt/systray.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Lock, Thread 3 | from typing import List 4 | 5 | from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt 6 | from PyQt5.QtGui import QIcon 7 | from PyQt5.QtWidgets import QSystemTrayIcon, QMenu 8 | 9 | from fpakman import __app_name__ 10 | from fpakman.core import resource, system 11 | from fpakman.core.controller import ApplicationManager 12 | from fpakman.core.model import ApplicationUpdate 13 | from fpakman.view.qt.about import AboutDialog 14 | from fpakman.view.qt.window import ManageWindow 15 | 16 | 17 | class UpdateCheck(QThread): 18 | 19 | signal = pyqtSignal(list) 20 | 21 | def __init__(self, manager: ApplicationManager, check_interval: int, parent=None): 22 | super(UpdateCheck, self).__init__(parent) 23 | self.check_interval = check_interval 24 | self.manager = manager 25 | 26 | def run(self): 27 | 28 | while True: 29 | updates = self.manager.list_updates() 30 | self.signal.emit(updates) 31 | time.sleep(self.check_interval) 32 | 33 | 34 | class TrayIcon(QSystemTrayIcon): 35 | 36 | def __init__(self, locale_keys: dict, manager: ApplicationManager, manage_window: ManageWindow, check_interval: int = 60, update_notification: bool = True): 37 | super(TrayIcon, self).__init__() 38 | self.locale_keys = locale_keys 39 | self.manager = manager 40 | 41 | self.icon_default = QIcon(resource.get_path('img/logo.png')) 42 | self.icon_update = QIcon(resource.get_path('img/logo_update.png')) 43 | self.setIcon(self.icon_default) 44 | 45 | self.menu = QMenu() 46 | 47 | self.action_manage = self.menu.addAction(self.locale_keys['tray.action.manage']) 48 | self.action_manage.triggered.connect(self.show_manage_window) 49 | 50 | self.action_about = self.menu.addAction(self.locale_keys['tray.action.about']) 51 | self.action_about.triggered.connect(self.show_about) 52 | 53 | self.action_exit = self.menu.addAction(self.locale_keys['tray.action.exit']) 54 | self.action_exit.triggered.connect(lambda: QCoreApplication.exit()) 55 | 56 | self.setContextMenu(self.menu) 57 | 58 | self.manage_window = None 59 | self.dialog_about = None 60 | self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager) 61 | self.check_thread.signal.connect(self.notify_updates) 62 | self.check_thread.start() 63 | 64 | self.last_updates = set() 65 | self.update_notification = update_notification 66 | self.lock_notify = Lock() 67 | 68 | self.activated.connect(self.handle_click) 69 | self.set_default_tooltip() 70 | 71 | self.manage_window = manage_window 72 | 73 | def set_default_tooltip(self): 74 | self.setToolTip('{} ({})'.format(self.locale_keys['manage_window.title'], __app_name__).lower()) 75 | 76 | def handle_click(self, reason): 77 | if reason == self.Trigger: 78 | self.show_manage_window() 79 | 80 | def verify_updates(self, notify_user: bool = True): 81 | Thread(target=self._verify_updates, args=(notify_user,)).start() 82 | 83 | def _verify_updates(self, notify_user: bool): 84 | self.notify_updates(self.manager.list_updates(), notify_user=notify_user) 85 | 86 | def notify_updates(self, updates: List[ApplicationUpdate], notify_user: bool = True): 87 | 88 | self.lock_notify.acquire() 89 | 90 | try: 91 | if len(updates) > 0: 92 | update_keys = {'{}:{}:{}'.format(up.type, up.id, up.version) for up in updates} 93 | 94 | new_icon = self.icon_update 95 | 96 | if update_keys.difference(self.last_updates): 97 | self.last_updates = update_keys 98 | msg = '{}: {}'.format(self.locale_keys['notification.new_updates'], len(updates)) 99 | self.setToolTip(msg) 100 | 101 | if self.update_notification and notify_user: 102 | system.notify_user(msg) 103 | 104 | else: 105 | self.last_updates.clear() 106 | new_icon = self.icon_default 107 | self.set_default_tooltip() 108 | 109 | if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed 110 | self.setIcon(new_icon) 111 | 112 | finally: 113 | self.lock_notify.release() 114 | 115 | def show_manage_window(self): 116 | if self.manage_window.isMinimized(): 117 | self.manage_window.setWindowState(Qt.WindowNoState) 118 | elif not self.manage_window.isVisible(): 119 | self.manage_window.refresh_apps() 120 | self.manage_window.show() 121 | 122 | def show_about(self): 123 | 124 | if self.dialog_about is None: 125 | self.dialog_about = AboutDialog(self.locale_keys) 126 | 127 | if self.dialog_about.isHidden(): 128 | self.dialog_about.show() 129 | -------------------------------------------------------------------------------- /fpakman/view/qt/thread.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | from datetime import datetime, timedelta 4 | from typing import List 5 | 6 | import requests 7 | from PyQt5.QtCore import QThread, pyqtSignal 8 | 9 | from fpakman.core.controller import ApplicationManager 10 | from fpakman.core.exception import NoInternetException 11 | from fpakman.core.model import ApplicationStatus 12 | from fpakman.core.system import FpakmanProcess 13 | from fpakman.util.cache import Cache 14 | from fpakman.view.qt import dialog 15 | from fpakman.view.qt.view_model import ApplicationView 16 | 17 | 18 | class AsyncAction(QThread): 19 | 20 | def notify_subproc_outputs(self, proc: FpakmanProcess, signal) -> bool: 21 | """ 22 | :param subproc: 23 | :param signal: 24 | :param success: 25 | :return: if the subprocess succeeded 26 | """ 27 | signal.emit(' '.join(proc.subproc.args) + '\n') 28 | 29 | success, already_succeeded = True, False 30 | 31 | for output in proc.subproc.stdout: 32 | line = output.decode().strip() 33 | if line: 34 | signal.emit(line) 35 | 36 | if proc.success_pgrase and proc.success_pgrase in line: 37 | already_succeeded = True 38 | 39 | if already_succeeded: 40 | return True 41 | 42 | for output in proc.subproc.stderr: 43 | line = output.decode().strip() 44 | if line: 45 | if proc.wrong_error_phrase and proc.wrong_error_phrase in line: 46 | continue 47 | else: 48 | success = False 49 | signal.emit(line) 50 | 51 | return success 52 | 53 | 54 | class UpdateSelectedApps(AsyncAction): 55 | 56 | signal_finished = pyqtSignal(bool, int) 57 | signal_status = pyqtSignal(str) 58 | signal_output = pyqtSignal(str) 59 | 60 | def __init__(self, manager: ApplicationManager, apps_to_update: List[ApplicationView] = None): 61 | super(UpdateSelectedApps, self).__init__() 62 | self.apps_to_update = apps_to_update 63 | self.manager = manager 64 | 65 | def run(self): 66 | 67 | success = False 68 | 69 | for app in self.apps_to_update: 70 | self.signal_status.emit(app.model.base_data.name) 71 | process = self.manager.update_and_stream(app.model) 72 | success = self.notify_subproc_outputs(process, self.signal_output) 73 | 74 | if not success: 75 | break 76 | else: 77 | self.signal_output.emit('\n') 78 | 79 | self.signal_finished.emit(success, len(self.apps_to_update)) 80 | self.apps_to_update = None 81 | 82 | 83 | class RefreshApps(QThread): 84 | 85 | signal = pyqtSignal(list) 86 | 87 | def __init__(self, manager: ApplicationManager): 88 | super(RefreshApps, self).__init__() 89 | self.manager = manager 90 | 91 | def run(self): 92 | self.signal.emit(self.manager.read_installed()) 93 | 94 | 95 | class UninstallApp(AsyncAction): 96 | signal_finished = pyqtSignal(object) 97 | signal_output = pyqtSignal(str) 98 | 99 | def __init__(self, manager: ApplicationManager, icon_cache: Cache, app: ApplicationView = None): 100 | super(UninstallApp, self).__init__() 101 | self.app = app 102 | self.manager = manager 103 | self.icon_cache = icon_cache 104 | self.root_password = None 105 | 106 | def run(self): 107 | if self.app: 108 | process = self.manager.uninstall_and_stream(self.app.model, self.root_password) 109 | success = self.notify_subproc_outputs(process, self.signal_output) 110 | 111 | if success: 112 | self.icon_cache.delete(self.app.model.base_data.icon_url) 113 | self.manager.clean_cache_for(self.app.model) 114 | 115 | self.signal_finished.emit(self.app if success else None) 116 | self.app = None 117 | self.root_password = None 118 | 119 | 120 | class DowngradeApp(AsyncAction): 121 | signal_finished = pyqtSignal(bool) 122 | signal_output = pyqtSignal(str) 123 | 124 | def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None): 125 | super(DowngradeApp, self).__init__() 126 | self.manager = manager 127 | self.app = app 128 | self.locale_keys = locale_keys 129 | self.root_password = None 130 | 131 | def run(self): 132 | if self.app: 133 | 134 | success = False 135 | try: 136 | process = self.manager.downgrade_app(self.app.model, self.root_password) 137 | 138 | if process is None: 139 | dialog.show_error(title=self.locale_keys['popup.downgrade.impossible.title'], 140 | body=self.locale_keys['popup.downgrade.impossible.body']) 141 | else: 142 | success = self.notify_subproc_outputs(process, self.signal_output) 143 | except (requests.exceptions.ConnectionError, NoInternetException): 144 | success = False 145 | self.signal_output.emit(self.locale_keys['internet.required']) 146 | finally: 147 | self.app = None 148 | self.root_password = None 149 | self.signal_finished.emit(success) 150 | 151 | 152 | class GetAppInfo(QThread): 153 | signal_finished = pyqtSignal(dict) 154 | 155 | def __init__(self, manager: ApplicationManager, app: ApplicationView = None): 156 | super(GetAppInfo, self).__init__() 157 | self.app = app 158 | self.manager = manager 159 | 160 | def run(self): 161 | if self.app: 162 | info = {'__app__': self.app} 163 | info.update(self.manager.get_info(self.app.model)) 164 | self.signal_finished.emit(info) 165 | self.app = None 166 | 167 | 168 | class GetAppHistory(QThread): 169 | signal_finished = pyqtSignal(dict) 170 | 171 | def __init__(self, manager: ApplicationManager, locale_keys: dict, app: ApplicationView = None): 172 | super(GetAppHistory, self).__init__() 173 | self.app = app 174 | self.manager = manager 175 | self.locale_keys = locale_keys 176 | 177 | def run(self): 178 | if self.app: 179 | try: 180 | res = {'model': self.app.model, 'history': self.manager.get_history(self.app.model)} 181 | self.signal_finished.emit(res) 182 | except (requests.exceptions.ConnectionError, NoInternetException): 183 | self.signal_finished.emit({'error': self.locale_keys['internet.required']}) 184 | finally: 185 | self.app = None 186 | 187 | 188 | class SearchApps(QThread): 189 | signal_finished = pyqtSignal(list) 190 | 191 | def __init__(self, manager: ApplicationManager): 192 | super(SearchApps, self).__init__() 193 | self.word = None 194 | self.manager = manager 195 | 196 | def run(self): 197 | apps_found = [] 198 | 199 | if self.word: 200 | res = self.manager.search(self.word) 201 | apps_found.extend(res['installed']) 202 | apps_found.extend(res['new']) 203 | 204 | self.signal_finished.emit(apps_found) 205 | self.word = None 206 | 207 | 208 | class InstallApp(AsyncAction): 209 | 210 | signal_finished = pyqtSignal(object) 211 | signal_output = pyqtSignal(str) 212 | 213 | def __init__(self, manager: ApplicationManager, disk_cache: bool, icon_cache: Cache, locale_keys: dict, app: ApplicationView = None): 214 | super(InstallApp, self).__init__() 215 | self.app = app 216 | self.manager = manager 217 | self.icon_cache = icon_cache 218 | self.disk_cache = disk_cache 219 | self.locale_keys = locale_keys 220 | self.root_password = None 221 | 222 | def run(self): 223 | 224 | if self.app: 225 | success = False 226 | 227 | try: 228 | process = self.manager.install_and_stream(self.app.model, self.root_password) 229 | success = self.notify_subproc_outputs(process, self.signal_output) 230 | 231 | if success and self.disk_cache: 232 | self.app.model.installed = True 233 | 234 | if self.app.model.supports_disk_cache(): 235 | icon_data = self.icon_cache.get(self.app.model.base_data.icon_url) 236 | self.manager.cache_to_disk(app=self.app.model, 237 | icon_bytes=icon_data.get('bytes') if icon_data else None, 238 | only_icon=False) 239 | except (requests.exceptions.ConnectionError, NoInternetException): 240 | success = False 241 | self.signal_output.emit(self.locale_keys['internet.required']) 242 | finally: 243 | self.signal_finished.emit(self.app if success else None) 244 | self.app = None 245 | 246 | 247 | class AnimateProgress(QThread): 248 | 249 | signal_change = pyqtSignal(int) 250 | 251 | def __init__(self): 252 | super(AnimateProgress, self).__init__() 253 | self.progress_value = 0 254 | self.increment = 5 255 | self.stop = False 256 | 257 | def run(self): 258 | 259 | current_increment = self.increment 260 | 261 | while not self.stop: 262 | self.signal_change.emit(self.progress_value) 263 | 264 | if self.progress_value == 100: 265 | current_increment = -current_increment 266 | if self.progress_value == 0: 267 | current_increment = self.increment 268 | 269 | self.progress_value += current_increment 270 | 271 | time.sleep(0.05) 272 | 273 | self.progress_value = 0 274 | 275 | 276 | class VerifyModels(QThread): 277 | 278 | signal_updates = pyqtSignal() 279 | 280 | def __init__(self, apps: List[ApplicationView] = None): 281 | super(VerifyModels, self).__init__() 282 | self.apps = apps 283 | 284 | def run(self): 285 | 286 | if self.apps: 287 | 288 | stop_at = datetime.utcnow() + timedelta(seconds=30) 289 | last_ready = 0 290 | 291 | while True: 292 | current_ready = 0 293 | 294 | for app in self.apps: 295 | current_ready += 1 if app.model.status == ApplicationStatus.READY else 0 296 | 297 | if current_ready > last_ready: 298 | last_ready = current_ready 299 | self.signal_updates.emit() 300 | 301 | if current_ready == len(self.apps): 302 | self.signal_updates.emit() 303 | break 304 | 305 | if stop_at <= datetime.utcnow(): 306 | break 307 | 308 | time.sleep(0.1) 309 | 310 | self.apps = None 311 | 312 | 313 | class RefreshApp(AsyncAction): 314 | 315 | signal_finished = pyqtSignal(bool) 316 | signal_output = pyqtSignal(str) 317 | 318 | def __init__(self, manager: ApplicationManager, app: ApplicationView = None): 319 | super(RefreshApp, self).__init__() 320 | self.app = app 321 | self.manager = manager 322 | self.root_password = None 323 | 324 | def run(self): 325 | 326 | if self.app: 327 | success = False 328 | 329 | try: 330 | process = self.manager.refresh(self.app.model, self.root_password) 331 | success = self.notify_subproc_outputs(process, self.signal_output) 332 | except (requests.exceptions.ConnectionError, NoInternetException): 333 | success = False 334 | self.signal_output.emit(self.locale_keys['internet.required']) 335 | finally: 336 | self.app = None 337 | self.signal_finished.emit(success) 338 | 339 | 340 | class FindSuggestions(AsyncAction): 341 | 342 | signal_finished = pyqtSignal(list) 343 | 344 | def __init__(self, man: ApplicationManager): 345 | super(FindSuggestions, self).__init__() 346 | self.man = man 347 | 348 | def run(self): 349 | self.signal_finished.emit(self.man.list_suggestions(limit=-1)) 350 | 351 | 352 | class ListWarnings(QThread): 353 | 354 | signal_warnings = pyqtSignal(list) 355 | 356 | def __init__(self, man: ApplicationManager, locale_keys: dict): 357 | super(QThread, self).__init__() 358 | self.locale_keys = locale_keys 359 | self.man = man 360 | 361 | def run(self): 362 | warnings = self.man.list_warnings() 363 | if warnings: 364 | self.signal_warnings.emit(warnings) 365 | -------------------------------------------------------------------------------- /fpakman/view/qt/view_model.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from fpakman.core.model import Application 4 | 5 | 6 | class ApplicationViewStatus(Enum): 7 | LOADING = 0 8 | READY = 1 9 | 10 | 11 | class ApplicationView: 12 | 13 | def __init__(self, model: Application, visible: bool = True): 14 | self.model = model 15 | self.update_checked = model.update 16 | self.visible = visible 17 | self.status = ApplicationViewStatus.LOADING 18 | 19 | def __repr__(self): 20 | return '{} ( {} )'.format(self.model.base_data.name, self.model.get_type()) 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyqt5>=5.12 2 | requests>=2.22 3 | colorama>=0.4.1 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | DESCRIPTION = ( 5 | "Graphical user interface to manage Flatpak / Snap applications." 6 | ) 7 | 8 | AUTHOR = "Vinicius Moreira" 9 | AUTHOR_EMAIL = "vinicius_fmoreira@hotmail.com" 10 | URL = "https://github.com/vinifmor/fpakman" 11 | 12 | file_dir = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | with open(file_dir + '/requirements.txt', 'r') as f: 15 | requirements = [line.strip() for line in f.readlines() if line] 16 | 17 | 18 | with open(file_dir + '/fpakman/__init__.py', 'r') as f: 19 | exec(f.readlines()[0]) 20 | 21 | 22 | setup( 23 | name="fpakman", 24 | version=eval('__version__'), 25 | description=DESCRIPTION, 26 | long_description=DESCRIPTION, 27 | author=AUTHOR, 28 | author_email=AUTHOR_EMAIL, 29 | python_requires=">=3.5", 30 | url=URL, 31 | packages=find_packages(), 32 | package_data={"fpakman": ["resources/locale/*", "resources/img/*"]}, 33 | install_requires=requirements, 34 | entry_points={ 35 | "console_scripts": [ 36 | "fpakman=fpakman.app" 37 | ] 38 | }, 39 | include_package_data=True, 40 | license="zlib/libpng", 41 | classifiers=[ 42 | 'Topic :: Utilities', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 3.5', 45 | 'Programming Language :: Python :: 3.6', 46 | 'Programming Language :: Python :: 3.7' 47 | ] 48 | ) 49 | --------------------------------------------------------------------------------