From 4903d36e02f3eaa9d7d539a1f72a07c69c232b4a Mon Sep 17 00:00:00 2001 From: heliguy4599 Date: Thu, 15 Feb 2024 03:21:57 -0500 Subject: [PATCH] Fully complete the Install From The Web window, for installing new paks --- PLANNED & NOT PLANNED.md | 2 +- data/icons/left-large-symbolic.svg | 2 + data/ui/orphans.blp | 2 +- data/ui/search_install.blp | 276 +++++++++++++++++---------- data/ui/window.blp | 30 +-- src/main.py | 11 +- src/search_install_window.py | 289 ++++++++++++++++++----------- src/warehouse.gresource.xml | 1 + src/window.py | 1 + 9 files changed, 383 insertions(+), 231 deletions(-) create mode 100644 data/icons/left-large-symbolic.svg diff --git a/PLANNED & NOT PLANNED.md b/PLANNED & NOT PLANNED.md index 20f7fa4..e119566 100644 --- a/PLANNED & NOT PLANNED.md +++ b/PLANNED & NOT PLANNED.md @@ -5,13 +5,13 @@ ### Features that have all work done and will release in the next update (ready to ship) - Remembering filters on app close +- Loading pages display the current working task, and how much is left ### Features that might release in the next update, but will release in some update - Installing flatpaks from a search - Edit flatpak pins - Uninstall unused packages -- Loading pages display the current working task, and how much is left # Features Being Considered diff --git a/data/icons/left-large-symbolic.svg b/data/icons/left-large-symbolic.svg new file mode 100644 index 0000000..e7e709d --- /dev/null +++ b/data/icons/left-large-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/ui/orphans.blp b/data/ui/orphans.blp index 982c31d..64ebcc6 100644 --- a/data/ui/orphans.blp +++ b/data/ui/orphans.blp @@ -87,7 +87,7 @@ template $OrphansWindow: Adw.Window { } Label installing_status { - label: _(""); + label: ""; justify: center; styles [ diff --git a/data/ui/search_install.blp b/data/ui/search_install.blp index b754430..d04de40 100644 --- a/data/ui/search_install.blp +++ b/data/ui/search_install.blp @@ -4,121 +4,195 @@ using Adw 1; template $SearchInstallWindow: Adw.Window { default-width: 500; default-height: 450; - title: _("Install From The Web…"); + title: ""; modal: true; Adw.ToolbarView main_toolbar_view { - [top] - HeaderBar header_bar { - show-title-buttons: false; - - [start] - Button cancel_button { - label: _("Cancel"); - } - - [end] - Button install_button { - label: _("Install"); - - styles [ - "suggested-action" - ] - } - } - - [top] - Adw.Clamp { - Box { - margin-top: 6; - margin-start: 6; - margin-end: 6; - - styles [ - "linked" - ] - - [start] - SearchEntry search_entry { - hexpand: true; - placeholder-text: _("Search for Flatpaks…"); - } - - [start] - MenuButton remotes_dropdown { - label: _("All Remotes"); - } - - [start] - Button search_button { - tooltip-text: _("Search"); - icon-name: "system-search-symbolic"; - } - } - } - - content: Adw.ToastOverlay toast_overlay { - Stack main_stack { - Overlay main_overlay { - [overlay] - ProgressBar progress_bar { - pulse-step: 0.7; - can-target: false; - - styles [ - "osd" - ] - } - - ScrolledWindow { - Adw.Clamp { - ListBox results_list_box { - margin-top: 12; - margin-bottom: 12; - margin-start: 12; - margin-end: 12; - hexpand: true; - valign: start; - selection-mode: none; - - styles [ - "boxed-list" - ] + content: + Stack outer_stack { + Adw.NavigationView nav_view { + Adw.NavigationPage search_page { + title: _("Search Criteria"); + Adw.ToastOverlay toast_overlay { + Adw.ToolbarView { + [top] + HeaderBar { } + content: + Adw.StatusPage { + title: _("Choose a Remote to Search"); + valign: start; + child: + Adw.Clamp { + ListBox remotes_list { + selection-mode: none; + styles ["boxed-list"] + } + }; + }; } } } + Adw.NavigationPage results_page { + title: _("Results"); + Adw.ToolbarView { + [top] + HeaderBar { + [start] + Button back_button { + tooltip-text: _("Back"); + icon-name: "left-large-symbolic"; + } + } + [bottom] + ActionBar action_bar { + revealed: false; + [center] + Button install_button { + margin-top: 6; + margin-bottom: 6; + styles[ + "pill", + "suggested-action" + ] - Adw.StatusPage no_results { - icon-name: "system-search-symbolic"; - title: _("No Results Found"); - description: _("Try a different search term"); - } + Adw.ButtonContent { + label: _("Install"); + icon-name: "plus-large-symbolic"; + } + } + } + [top] + Adw.Clamp { + Box search_box { + margin-top: 4; + margin-start: 12; + margin-end: 12; + margin-bottom: 6; + SearchEntry search_entry { + hexpand: true; + } + Button search_button { + icon-name: "right-large-symbolic"; + tooltip-text: _("Start Search"); + } + styles ["linked"] + } + } + content: + Stack inner_stack { - Adw.StatusPage blank_page { - title: _("Search for Flatpaks"); - icon-name: "flatpak-symbolic"; - description: _("Search for Flatpaks that you want to install"); - } + Adw.StatusPage blank_page { + title: _("Search for Flatpaks"); + icon-name: "flatpak-symbolic"; + description: _("Search for Flatpaks that you want to install"); + } - Adw.StatusPage loading_page { - title: C_("Shown with a spinner while search operation is pending", "Searching"); + Adw.StatusPage no_results { + icon-name: "system-search-symbolic"; + title: _("No Results Found"); + description: _("Try a different search term"); + } - Spinner { - spinning: true; - height-request: 32; - width-request: 32; - margin-top: 0; - halign: center; - valign: center; + Box loading_page { + orientation: vertical; + spacing: 10; + margin-top: 40; + margin-bottom: 20; + halign: center; + valign: center; + + Spinner { + margin-bottom: 35; + width-request: 30; + height-request: 30; + opacity: 0.5; + spinning: true; + } + + Label { + label: _("Searching"); + + styles [ + "title-1", + "title" + ] + } + } + + Adw.StatusPage too_many { + icon-name: "error-symbolic"; + title: _("Too Many Results"); + description: _("Try being more specific with your search"); + } + + ScrolledWindow results_scroll { + vexpand: true; + Adw.Clamp { + ListBox results_list { + margin-top: 6; + margin-bottom: 12; + margin-start: 12; + margin-end: 12; + hexpand: true; + valign: start; + selection-mode: none; + styles ["boxed-list"] + } + } + } + }; } } - - Adw.StatusPage too_many { - icon-name: "error-symbolic"; - title: _("Too Many Results"); - description: _("Try being more specific with your search"); + } + Adw.ToolbarView installing { + [top] + HeaderBar { } + content: + Overlay overlay { + [overlay] + ProgressBar progress_bar { + visible: false; + can-target: false; + styles ["osd"] + } + Box { + orientation: vertical; + spacing: 10; + margin-top: 40; + margin-bottom: 20; + halign: center; + valign: center; + + Spinner { + margin-bottom: 35; + width-request: 30; + height-request: 30; + opacity: 0.5; + spinning: true; + } + + Label { + label: _("Installing"); + + styles [ + "title-1", + "title" + ] + } + + Label installing_status { + label: ""; + justify: center; + + styles [ + "description", + "body" + ] + } + } + }; } }; } diff --git a/data/ui/window.blp b/data/ui/window.blp index 7564be0..bdfd1fb 100644 --- a/data/ui/window.blp +++ b/data/ui/window.blp @@ -36,6 +36,13 @@ template $WarehouseWindow: Adw.ApplicationWindow { icon-name: "selection-mode-symbolic"; tooltip-text: _("Toggle Selection Mode"); } + + [end] + MenuButton install_button { + icon-name: "plus-large-symbolic"; + tooltip-text: _("Install New Flatpak"); + menu-model: install_menu; + } } [top] @@ -137,7 +144,7 @@ template $WarehouseWindow: Adw.ApplicationWindow { } Label uninstalling_status { - label: _(""); + label: ""; justify: center; styles ["description", "body"] } @@ -237,11 +244,6 @@ template $WarehouseWindow: Adw.ApplicationWindow { menu primary_menu { section { - item { - label: _("Install From File…"); - action: "app.install-from-file"; - } - item { label: _("Manage Leftover Data…"); action: "app.manage-data-folders"; @@ -256,11 +258,6 @@ menu primary_menu { action: "app.show-remotes-window"; } - // item { - // label: _("Install From The Web…"); - // action: "app.open-search-install"; - // } - item { label: _("_Keyboard Shortcuts"); action: "win.show-help-overlay"; @@ -292,11 +289,16 @@ menu copy_menu { } } -menu row_menu { +menu install_menu { section { item { - label: _("Open App"); - //action: "win.open-app"; + label: _("Install From File…"); + action: "app.install-from-file"; + } + + item { + label: _("Install From The Web…"); + action: "app.open-search-install"; } } } diff --git a/src/main.py b/src/main.py index 67b315d..4a4e926 100644 --- a/src/main.py +++ b/src/main.py @@ -32,6 +32,7 @@ from .orphans_window import OrphansWindow from .filter_window import FilterWindow from .search_install_window import SearchInstallWindow from .const import Config +from .common import myUtils class WarehouseApplication(Adw.Application): @@ -70,7 +71,7 @@ class WarehouseApplication(Adw.Application): self.create_action("set-filter", self.filters_shortcut, ["t"]) self.create_action("install-from-file", self.install_from_file, ["o"]) self.create_action("open-menu", self.main_menu_shortcut, ["F10"]) - # self.create_action("open-search-install", self.open_search_install, ["i"]) + self.create_action("open-search-install", self.open_search_install, ["i"]) gtk_version = ( str(Gtk.MAJOR_VERSION) @@ -99,6 +100,14 @@ class WarehouseApplication(Adw.Application): lang=lang, ) + my_utils = myUtils(self) + total = 0 + for rem in my_utils.get_host_remotes(): + if my_utils.get_install_type(rem[7]) != "disabled": + total += 1 + if total < 2: + self.lookup_action(f"open-search-install").set_enabled(False) + def open_search_install(self, widget, _): SearchInstallWindow(self.props.active_window).present() diff --git a/src/search_install_window.py b/src/search_install_window.py index 9d36d85..adc9c3d 100644 --- a/src/search_install_window.py +++ b/src/search_install_window.py @@ -4,6 +4,34 @@ import subprocess import os import pathlib +class RemoteRow(Adw.ActionRow): + def __init__(self, remote, **kwargs): + super().__init__(**kwargs) + my_utils = myUtils(self) + self.install_type = my_utils.get_install_type(remote[7]) + if self.install_type == "disabled": + self.set_visible(False) + return + self.set_activatable(True) + self.remote = remote + if remote[1] == "-": + self.set_title(remote[0]) + else: + self.set_title(remote[1]) + self.set_subtitle(_("{} wide").format(self.install_type)) + self.add_suffix(Gtk.Image.new_from_icon_name("right-large-symbolic")) + +class ResultRow(Adw.ActionRow): + def __init__(self, flatpak, **kwargs): + super().__init__(**kwargs) + my_utils = myUtils(self) + self.flatpak = flatpak + self.set_title(GLib.markup_escape_text(flatpak[0])) + self.set_subtitle(GLib.markup_escape_text(flatpak[1])) + self.check = Gtk.CheckButton() + self.check.add_css_class("selection-mode") + self.add_suffix(self.check) + self.set_activatable_widget(self.check) @Gtk.Template( resource_path="/io/github/flattool/Warehouse/../data/ui/search_install.ui" @@ -13,144 +41,179 @@ class SearchInstallWindow( ): # TODO: stop execution of thread when search is changed __gtype_name__ = "SearchInstallWindow" - results_list_box = Gtk.Template.Child() - main_stack = Gtk.Template.Child() - main_overlay = Gtk.Template.Child() - no_results = Gtk.Template.Child() - too_many = Gtk.Template.Child() - cancel_button = Gtk.Template.Child() - blank_page = Gtk.Template.Child() - loading_page = Gtk.Template.Child() - search_button = Gtk.Template.Child() + back_button = Gtk.Template.Child() + nav_view = Gtk.Template.Child() + search_page = Gtk.Template.Child() + results_page = Gtk.Template.Child() + remotes_list = Gtk.Template.Child() search_entry = Gtk.Template.Child() - remotes_dropdown = Gtk.Template.Child() - - is_debug = GLib.environ_getenv(GLib.get_environ(), "G_MESSAGES_DEBUG") == "all" + blank_page = Gtk.Template.Child() + inner_stack = Gtk.Template.Child() + outer_stack = Gtk.Template.Child() + loading_page = Gtk.Template.Child() + results_scroll = Gtk.Template.Child() + results_list = Gtk.Template.Child() + too_many = Gtk.Template.Child() + action_bar = Gtk.Template.Child() + search_button = Gtk.Template.Child() + no_results = Gtk.Template.Child() + install_button = Gtk.Template.Child() + installing = Gtk.Template.Child() + installing_status = Gtk.Template.Child() + search_box = Gtk.Template.Child() + toast_overlay = Gtk.Template.Child() + progress_bar = Gtk.Template.Child() def key_handler(self, controller, keyval, keycode, state): if keyval == Gdk.KEY_Escape or (keyval == Gdk.KEY_w and state == Gdk.ModifierType.CONTROL_MASK): self.close() - def search_response(self, a, b): - self.results_list_box.remove_all() - print(self.search_results) - if (self.is_debug and len(self.search_results) == 5) or ( - len(self.search_results) == 1 and len(self.search_results[0]) == 1 - ): # This is unreliable with G_DEBUG - self.main_stack.set_visible_child(self.no_results) + def reset(self): + self.results = [] + self.results_list.remove_all() + self.inner_stack.set_visible_child(self.blank_page) + + def check_handler(self, button, row): + if button.get_active(): + self.selected.append(row.flatpak) + else: + self.selected.remove(row.flatpak) + if len(self.selected) == 0: + self.set_title(self.title) + self.action_bar.set_revealed(False) + else: + self.set_title(_("{} Selected").format(len(self.selected))) + self.action_bar.set_revealed(True) + + def generate_remotes_list(self): + total = 0 + for rem in self.host_remotes: + if self.my_utils.get_install_type(rem[7]) != "disabled": + total += 1 + if total < 2: + self.nav_view.push(self.results_page) + self.back_button.set_visible(False) + self.back_button.set_sensitive(False) + self.search_remote = self.host_remotes[0][0] + self.install_type = self.host_remotes[0][7] + self.nav_view.connect("popped", lambda *_: self.nav_view.push(self.results_page)) + self.nav_view.set_animate_transitions(False) + self.title = _("Search {}").format(self.search_remote) + self.set_title(self.title) + self.search_entry.set_placeholder_text(_("Search {}").format(self.search_remote)) + self.search_entry.grab_focus() return - if len(self.search_results) > 50: - self.main_stack.set_visible_child(self.too_many) + self.nav_view.connect("popped", lambda *_: self.set_title("")) + for remote in self.host_remotes: + row = RemoteRow(remote) + row.connect("activated", self.remote_choice) + self.remotes_list.append(row) + + def generate_results_list(self): + for pak in self.results: + row = ResultRow(pak) + row.check.set_active(row.flatpak in self.selected) + row.check.connect("toggled", self.check_handler, row) + if self.search_remote in row.flatpak[5]: + self.results_list.append(row) + if self.results_list.get_row_at_index(0): + self.inner_stack.set_visible_child(self.results_scroll) + else: + self.inner_stack.set_visible_child(self.no_results) + + def remote_choice(self, row): + self.reset() + self.selected = [] + self.install_type = row.install_type + self.search_remote = row.remote[0] + self.search_entry.set_placeholder_text(_("Search {}").format(self.search_remote)) + self.title = _("Search {}").format(self.search_remote) + self.set_title(self.title) + self.nav_view.push(self.results_page) + self.search_entry.grab_focus() + self.action_bar.set_revealed(len(self.selected) > 0) + + def search_handler(self, *args): + self.reset() + self.inner_stack.set_visible_child(self.loading_page) + query = self.search_entry.get_text().strip() + if query == "": + self.inner_stack.set_visible_child(self.blank_page) return - self.main_stack.set_visible_child(self.main_overlay) - for i in range(len(self.search_results)): - try: - print("creating row {}".format(str(i))) - row = Adw.ActionRow( - title=GLib.markup_escape_text(self.search_results[i][0]), - subtitle=self.search_results[i][2], - ) - print("row {} is {}".format(str(i), self.search_results[i][0])) - check = Gtk.CheckButton() - check.add_css_class("selection-mode") - check.connect("toggled", self.on_check) - label = Gtk.Label( - label=self.search_results[i][3], - justify=Gtk.Justification.RIGHT, - wrap=True, - hexpand=True, - ) - row.add_suffix(label) - row.add_suffix(check) - row.set_activatable_widget(check) - self.results_list_box.append(row) - except: - print("Could not create row") + def search_thread(*args): + command = ['flatpak-spawn', '--host', 'flatpak', 'search', '--columns=all', query] + output = subprocess.run( + command, capture_output=True, text=True, env=self.new_env + ).stdout.strip().split('\n') + for elm in output: + self.results.append(elm.split("\t")) - def on_check(self, button): - print(button.get_active()) + def done(*args): + if len(self.results) > 50: + self.inner_stack.set_visible_child(self.too_many) + return + if ['No matches found'] in self.results: + self.inner_stack.set_visible_child(self.no_results) + return + self.generate_results_list() - def search_thread(self): - command = [ - "flatpak-spawn", - "--host", - "flatpak", - "search", - "--columns=all", - self.to_search, - ] - if self.remote_to_search: - command += self.remote_to_search + task = Gio.Task.new(None, None, done) + task.run_in_thread(search_thread) - output = subprocess.run( - command, capture_output=True, text=True, env=self.new_env - ).stdout - lines = output.strip().split("\n") - columns = lines[0].split("\t") - data = [columns] - for line in lines[1:]: - row = line.split("\t") - data.append(row) - data = sorted(data, key=lambda item: item[0].lower()) - self.search_results = data + def install_handler(self, *args): + paks = [] + for pak in self.selected: + paks.append(pak[2]) + self.outer_stack.set_visible_child(self.installing) + self.set_title(_("Install From The Web")) - def on_search(self, widget): - self.main_stack.set_visible_child(self.loading_page) - self.to_search = self.search_entry.get_text() - if len(self.to_search) < 1 or " " in self.to_search: - self.results_list_box.remove_all() - self.main_stack.set_visible_child(self.blank_page) - return - task = Gio.Task.new(None, None, self.search_response) - task.run_in_thread(lambda *_: self.search_thread()) + def thread(*args): + self.my_utils.install_flatpak(paks, self.search_remote, self.install_type, self.progress_bar, self.installing_status) - def set_choice(self, index): - print(index) - - def remotes_chooser_creator(self): - remotes_pop = Gtk.Popover() - remotes_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - # remotes_pop.set_size_request(400, 1) # why? - remotes_pop.set_child(remotes_box) # don't use ScrolledWindows in popovers! - - # why not just comment this code out and leave it unimplemented— it does nothing of use - for i in range(1, 3): - x = 0 - height = remotes_pop.get_size(x) - all = Gtk.Button(label="all") - all.add_css_class("flat") - remotes_box.append(all) - print(x) - - self.remotes_dropdown.set_popover(remotes_pop) + def done(*args): + self.parent_window.refresh_list_of_flatpaks(None, False) + self.disconnect(self.no_close_id) # Make window able to close + if self.my_utils.install_success: + self.close() + self.parent_window.toast_overlay.add_toast(Adw.Toast.new(_("Installed successfully"))) + else: + self.progress_bar.set_visible(False) + self.nav_view.pop() + self.outer_stack.set_visible_child(self.nav_view) + self.toast_overlay.add_toast(Adw.Toast.new(_("Some apps didn't install"))) + + self.no_close_id = self.connect( + "close-request", lambda event: True + ) # Make window unable to close + task = Gio.Task.new(None, None, done) + task.run_in_thread(thread) def __init__(self, parent_window, **kwargs): super().__init__(**kwargs) # Create Variables self.my_utils = myUtils(self) - self.search_results = [] - self.to_search = "" self.new_env = dict(os.environ) self.new_env["LC_ALL"] = "C" event_controller = Gtk.EventControllerKey() event_controller.connect("key-pressed", self.key_handler) - self.cancel_button.connect("clicked", lambda *_: self.close()) + self.host_remotes = self.my_utils.get_host_remotes() self.parent_window = parent_window + self.results = [] + self.selected = [] + self.search_remote = "" + self.install_type = "" + self.title = _("Install From The Web") + + self.back_button.connect("clicked", lambda *_: self.nav_view.pop()) + self.search_entry.connect("activate", self.search_handler) + self.search_button.connect("clicked", self.search_handler) + self.install_button.connect("clicked", self.install_handler) # Apply Widgets self.add_controller(event_controller) self.set_transient_for(parent_window) - # self.search_bar.connect_entry(self.search_entry) - self.search_entry.connect("activate", self.on_search) - self.search_button.connect("clicked", self.on_search) - self.search_entry.connect("changed", lambda *_: self.search_entry.grab_focus()) - # self.search_entry.set_key_capture_widget(self.results_list_box) - self.search_entry.grab_focus() - - self.host_remotes = self.my_utils.get_host_remotes() - if len(self.host_remotes) > 1: - self.remotes_chooser_creator() - - self.remote_to_search = [] - self.main_stack.set_visible_child(self.blank_page) + self.generate_remotes_list() + self.set_size_request(260, 230) + self.set_modal(True) + self.set_resizable(True) diff --git a/src/warehouse.gresource.xml b/src/warehouse.gresource.xml index 79fa82f..a2034aa 100644 --- a/src/warehouse.gresource.xml +++ b/src/warehouse.gresource.xml @@ -34,5 +34,6 @@ ../data/icons/important-small-symbolic.svg ../data/icons/eye-not-looking-symbolic.svg ../data/icons/eye-open-negative-filled-symbolic.svg + ../data/icons/left-large-symbolic.svg diff --git a/src/window.py b/src/window.py index 319a889..cea3783 100644 --- a/src/window.py +++ b/src/window.py @@ -67,6 +67,7 @@ class WarehouseWindow(Adw.ApplicationWindow): no_matches = Gtk.Template.Child() reset_filters_button = Gtk.Template.Child() uninstalling_status = Gtk.Template.Child() + install_button = Gtk.Template.Child() main_progress_bar = Gtk.ProgressBar(visible=False, can_target=False) main_progress_bar.add_css_class("osd")