├── .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 |
--------------------------------------------------------------------------------
/fpakman/resources/img/app_info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/fpakman/resources/img/app_settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/fpakman/resources/img/app_update.svg:
--------------------------------------------------------------------------------
1 |
2 |
77 |
--------------------------------------------------------------------------------
/fpakman/resources/img/checked.svg:
--------------------------------------------------------------------------------
1 |
2 |
63 |
--------------------------------------------------------------------------------
/fpakman/resources/img/downgrade.svg:
--------------------------------------------------------------------------------
1 |
2 |
73 |
--------------------------------------------------------------------------------
/fpakman/resources/img/exclamation.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/fpakman/resources/img/flathub.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
180 |
--------------------------------------------------------------------------------
/fpakman/resources/img/install.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/fpakman/resources/img/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/fpakman/resources/img/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/fpakman/resources/img/snapcraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vinifmor/fpakman/a719991b8f7ecf366d44fdf074f5950767bdf121/fpakman/resources/img/snapcraft.png
--------------------------------------------------------------------------------
/fpakman/resources/img/uninstall.svg:
--------------------------------------------------------------------------------
1 |
2 |
97 |
--------------------------------------------------------------------------------
/fpakman/resources/img/update_green.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------