diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2a007c..aab61532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - a confirmation popup is shown when the "install" / "upgrade all" button is clicked - "About" button added to the manage panel - "Updates" icon left-aligned +- show application suggestions when no app is installed. Can be enabled / disabled through the environment variable / parameter FPAKMAN_SUGGESTIONS (--sugs) - minor GUI improvements ### Fixes: - apps table not showing empty descriptions diff --git a/README.md b/README.md index c8f022b3..e5edc104 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ You can change some application settings via environment variables or arguments - **FPAKMAN_SNAP**: enables / disables snap usage. Use **0** (disable) or **1** (enabled, default) - **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). - **FPAKMAN_TRAY**: If the tray icon and update-check daemon should be created. Use **0** (disable) or **1** (enable, default). +- **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). ### How to improve the application performance - 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 @@ -60,6 +61,7 @@ about **snaps**, I can initialize the application setting "snap=0" (**fpakman -- with the technology that I don't care every time an action is executed. - 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. - 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. +- If you don't mind app suggestions, disable it (**fpakman --sugs=0**) ### Roadmap - Support for other packaging technologies diff --git a/fpakman/app.py b/fpakman/app.py index 25a8265e..89d6e8da 100755 --- a/fpakman/app.py +++ b/fpakman/app.py @@ -47,6 +47,7 @@ parser.add_argument('--flatpak', action="store", default=os.getenv('FPAKMAN_FLAT 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') 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') parser.add_argument('--tray', action="store", default=os.getenv('FPAKMAN_TRAY', 1), choices=[0, 1], type=int, help='If the tray icon and update-check daemon should be created. Default: %(default)s') +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') args = parser.parse_args() if args.cache_exp < 0: @@ -78,6 +79,9 @@ if args.download_icons == 0: if args.check_packaging_once == 1: log_msg("'check-packaging-once' is enabled", Fore.YELLOW) +if args.sugs == 0: + log_msg("suggestions are disabled", Fore.YELLOW) + locale_keys = util.get_locale_keys(args.locale) http_session = requests.Session() @@ -122,7 +126,8 @@ manage_window = ManageWindow(locale_keys=locale_keys, icon_cache=icon_cache, disk_cache=args.disk_cache, download_icons=bool(args.download_icons), - screen_size=screen_size) + screen_size=screen_size, + suggestions=args.sugs) if args.tray: trayIcon = TrayIcon(locale_keys=locale_keys, diff --git a/fpakman/core/controller.py b/fpakman/core/controller.py index 99161e8e..fd09417d 100755 --- a/fpakman/core/controller.py +++ b/fpakman/core/controller.py @@ -90,6 +90,10 @@ class ApplicationManager(ABC): def list_warnings(self) -> List[str]: pass + @abstractmethod + def list_suggestions(self, limit: int) -> List[Application]: + pass + class GenericApplicationManager(ApplicationManager): @@ -285,3 +289,11 @@ class GenericApplicationManager(ApplicationManager): warnings.extend(man_warnings) return warnings + + def list_suggestions(self, limit: int) -> List[Application]: + if self.managers: + suggestions = [] + for man in self.managers: + if self._is_enabled(man): + suggestions.extend(man.list_suggestions(5)) + return suggestions diff --git a/fpakman/core/flatpak/controller.py b/fpakman/core/flatpak/controller.py index 83292e4e..eec8dbf2 100644 --- a/fpakman/core/flatpak/controller.py +++ b/fpakman/core/flatpak/controller.py @@ -46,7 +46,9 @@ class FlatpakManager(ApplicationManager): if not api_data or expired_data: if not app_json['runtime']: - disk_loader.add(app) # preloading cached disk data + if disk_loader: + disk_loader.add(app) # preloading cached disk data + FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, http_session=self.http_session).start() else: @@ -181,3 +183,22 @@ class FlatpakManager(ApplicationManager): def list_warnings(self) -> List[str]: if not flatpak.has_remotes_set(): return [self.locale_keys['flatpak.notification.no_remotes']] + + def list_suggestions(self, limit: int) -> List[FlatpakApplication]: + + res = [] + + if limit != 0: + + for app_id in ('com.spotify.Client', 'com.skype.Client', 'com.dropbox.Client', 'us.zoom.Zoom', 'com.visualstudio.code', 'org.inkscape.Inkscape', 'org.libretro.RetroArch', 'org.kde.kdenlive', 'org.videolan.VLC'): + + app_json = flatpak.search(app_id, app_id=True) + + if app_json: + res.append(self._map_to_model(app_json[0], False, None)) + + if len(res) == limit: + break + + return res + diff --git a/fpakman/core/flatpak/flatpak.py b/fpakman/core/flatpak/flatpak.py index 7b6a0646..410064d5 100755 --- a/fpakman/core/flatpak/flatpak.py +++ b/fpakman/core/flatpak/flatpak.py @@ -153,7 +153,7 @@ def get_app_commits_data(app_ref: str, origin: str) -> List[dict]: return commits -def search(word: str) -> List[dict]: +def search(word: str, app_id: bool = False) -> List[dict]: cli_version = get_version() res = system.run_cmd('{} search {}'.format(BASE_CMD, word)) @@ -166,13 +166,17 @@ def search(word: str) -> List[dict]: for info in split_res: if info: info_list = info.split('\t') - if cli_version >= '1.3.0': + id_ = info_list[2].strip() + + if app_id and id_ != word: + continue + version = info_list[3].strip() - found.append({ + app = { 'name': info_list[0].strip(), 'description': info_list[1].strip(), - 'id': info_list[2].strip(), + 'id': id_, 'version': version, 'latest_version': version, 'branch': info_list[4].strip(), @@ -180,14 +184,19 @@ def search(word: str) -> List[dict]: 'runtime': False, 'arch': None, # unknown at this moment, 'ref': None # unknown at this moment - }) + } elif cli_version >= '1.2.0': + id_ = info_list[1].strip() + + if app_id and id_ != word: + continue + desc = info_list[0].split('-') version = info_list[2].strip() - found.append({ + app = { 'name': desc[0].strip(), 'description': desc[1].strip(), - 'id': info_list[1].strip(), + 'id': id_, 'version': version, 'latest_version': version, 'branch': info_list[3].strip(), @@ -195,13 +204,18 @@ def search(word: str) -> List[dict]: 'runtime': False, 'arch': None, # unknown at this moment, 'ref': None # unknown at this moment - }) + } else: + id_ = info_list[0].strip() + + if app_id and id_ != word: + continue + version = info_list[1].strip() - found.append({ + app = { 'name': '', 'description': info_list[4].strip(), - 'id': info_list[0].strip(), + 'id': id_, 'version': version, 'latest_version': version, 'branch': info_list[2].strip(), @@ -209,7 +223,13 @@ def search(word: str) -> List[dict]: 'runtime': False, 'arch': None, # unknown at this moment, 'ref': None # unknown at this moment - }) + } + + found.append(app) + + if app_id and len(found) > 0: + break + return found diff --git a/fpakman/core/snap/controller.py b/fpakman/core/snap/controller.py index 9052ef48..593adeb1 100644 --- a/fpakman/core/snap/controller.py +++ b/fpakman/core/snap/controller.py @@ -137,3 +137,18 @@ class SnapManager(ApplicationManager): def list_warnings(self) -> List[str]: if snap.get_snapd_version() == 'unavailable': return [self.locale_keys['snap.notification.snapd_unavailable']] + + def list_suggestions(self, limit: int) -> List[SnapApplication]: + + suggestions = [] + + if limit != 0: + for name in ('whatsdesk', 'slack', 'yakyak', 'instagraph', 'eclipse', 'gimp', 'supertuxkart'): + res = snap.search(name, exact_name=True) + if res: + suggestions.append(self.map_json(res[0], installed=False, disk_loader=None)) + + if len(suggestions) == limit: + break + + return suggestions diff --git a/fpakman/core/snap/snap.py b/fpakman/core/snap/snap.py index af01c5ac..5421d9f0 100644 --- a/fpakman/core/snap/snap.py +++ b/fpakman/core/snap/snap.py @@ -86,7 +86,7 @@ def read_installed() -> List[dict]: return apps -def search(word: str) -> List[dict]: +def search(word: str, exact_name: bool = False) -> List[dict]: apps = [] res = system.run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False) @@ -98,6 +98,10 @@ def search(word: str) -> List[dict]: for idx, app_str in enumerate(res): if idx > 0 and app_str: app_data = [word for word in app_str.split(' ') if word] + + if exact_name and app_data[0] != word: + continue + apps.append({ 'name': app_data[0], 'version': app_data[1], @@ -109,6 +113,9 @@ def search(word: str) -> List[dict]: 'type': None }) + if exact_name and len(apps) > 0: + break + return apps diff --git a/fpakman/view/qt/thread.py b/fpakman/view/qt/thread.py index f0506167..3ad1fde1 100644 --- a/fpakman/view/qt/thread.py +++ b/fpakman/view/qt/thread.py @@ -335,3 +335,16 @@ class RefreshApp(AsyncAction): finally: self.app = None self.signal_finished.emit(success) + + +class FindSuggestions(AsyncAction): + + signal_finished = pyqtSignal(list) + + def __init__(self, man: ApplicationManager): + super(FindSuggestions, self).__init__() + self.man = man + + def run(self): + self.signal_finished.emit(self.man.list_suggestions(limit=-1)) + \ No newline at end of file diff --git a/fpakman/view/qt/window.py b/fpakman/view/qt/window.py index 75109458..a3a4a89a 100755 --- a/fpakman/view/qt/window.py +++ b/fpakman/view/qt/window.py @@ -18,7 +18,7 @@ from fpakman.view.qt.history import HistoryDialog from fpakman.view.qt.info import InfoDialog from fpakman.view.qt.root import is_root, ask_root_password from fpakman.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ - GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp + GetAppHistory, SearchApps, InstallApp, AnimateProgress, VerifyModels, RefreshApp, FindSuggestions from fpakman.view.qt.view_model import ApplicationView DARK_ORANGE = '#FF4500' @@ -27,7 +27,7 @@ DARK_ORANGE = '#FF4500' class ManageWindow(QWidget): __BASE_HEIGHT__ = 400 - def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, tray_icon=None): + def __init__(self, locale_keys: dict, icon_cache: Cache, manager: ApplicationManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, tray_icon=None): super(ManageWindow, self).__init__() self.locale_keys = locale_keys self.manager = manager @@ -177,6 +177,9 @@ class ManageWindow(QWidget): self.thread_refresh_app.signal_finished.connect(self._finish_refresh) self.thread_refresh_app.signal_output.connect(self._update_action_output) + self.thread_suggestions = FindSuggestions(man=self.manager) + self.thread_suggestions.signal_finished.connect(self._finish_search) + self.toolbar_bottom = QToolBar() self.toolbar_bottom.setIconSize(QSize(16, 16)) @@ -208,6 +211,7 @@ class ManageWindow(QWidget): self._maximized = False self.dialog_about = None + self.first_refresh = suggestions def _show_about(self): if self.dialog_about is None: @@ -286,6 +290,7 @@ class ManageWindow(QWidget): self.ref_checkbox_only_apps.setVisible(True) self.ref_bt_upgrade.setVisible(True) self.update_apps(apps) + self.first_refresh = False def uninstall_app(self, app: ApplicationView): pwd = None @@ -457,8 +462,14 @@ class ManageWindow(QWidget): self.apps.append(app_model) if napps == 0: - self.checkbox_only_apps.setChecked(False) - self.checkbox_only_apps.setCheckable(False) + + if self.first_refresh: + self._begin_search('') + self.thread_suggestions.start() + return + else: + self.checkbox_only_apps.setChecked(False) + self.checkbox_only_apps.setCheckable(False) else: self.checkbox_only_apps.setCheckable(True) self.checkbox_only_apps.setChecked(True) @@ -626,16 +637,19 @@ class ManageWindow(QWidget): dialog_history = HistoryDialog(app, self.icon_cache, self.locale_keys) dialog_history.exec_() + def _begin_search(self, word): + self._handle_console_option(False) + self.ref_checkbox_only_apps.setVisible(False) + self.ref_checkbox_updates.setVisible(False) + self.filter_updates = False + self._begin_action('{}{}'.format(self.locale_keys['manage_window.status.searching'], '"{}"'.format(word) if word else ''), clear_filters=True) + def search(self): word = self.input_search.text().strip() if word: - self._handle_console_option(False) - self.ref_checkbox_only_apps.setVisible(False) - self.ref_checkbox_updates.setVisible(False) - self.filter_updates = False - self._begin_action('{} "{}"'.format(self.locale_keys['manage_window.status.searching'], word), clear_filters=True) + self._begin_search(word) self.thread_search.word = word self.thread_search.start()