diff --git a/src/common.py b/src/common.py index b9e5616..ca9c669 100644 --- a/src/common.py +++ b/src/common.py @@ -8,6 +8,8 @@ class myUtils: self.main_window = window self.host_home = str(pathlib.Path.home()) self.user_data_path = self.host_home + "/.var/app/" + self.install_success = True + self.uninstall_success = True def trashFolder(self, path): if not os.path.exists(path): @@ -18,6 +20,9 @@ class myUtils: except subprocess.CalledProcessError: return 2 + def getSizeWithFormat(self, path): + return self.getSizeFormat(self.getDirectorySize(path)) + def getSizeFormat(self, b): factor = 1024 suffix = "B" @@ -68,4 +73,98 @@ class myUtils: else: image = Gtk.Image.new_from_icon_name("application-x-executable-symbolic") image.set_icon_size(Gtk.IconSize.LARGE) - return image \ No newline at end of file + return image + + def getHostRemotes(self): + output = subprocess.run(["flatpak-spawn", "--host", "flatpak", "remotes", "--columns=all"], capture_output=True, text=True).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) + return data + + def getHostFlatpaks(self): + output = subprocess.run(["flatpak-spawn", "--host", "flatpak", "list", "--columns=all"], capture_output=True, text=True).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) + return data + + def uninstallFlatpak(self, ref_arr, type_arr, should_trash): + self.uninstall_success = True + + to_uninstall = [] + for i in range(len(ref_arr)): + to_uninstall.append([ref_arr[i], type_arr[i]]) + + apps = [] + fails = [] + for i in range(len(to_uninstall)): + ref = to_uninstall[i][0] + id = to_uninstall[i][0].split("/")[0] + app_type = to_uninstall[i][1] + apps.append([ref, id, app_type]) + # apps array guide: [app_ref, app_id, user_or_system_install] + + for i in range(len(apps)): + command = ['flatpak-spawn', '--host', 'flatpak', 'remove', '-y', f"--{apps[i][2]}", apps[i][0]] + try: + subprocess.run(command, capture_output=False, check=True) + except subprocess.CalledProcessError: + fails.append(apps[i]) + + if len(fails) > 0: # Run this only if there is 1 or more non uninstalled apps + pk_command = ['flatpak-spawn', '--host', 'pkexec', 'flatpak', 'remove', '-y', '--system'] + print("second uninstall process") + for i in range(len(fails)): + + if fails[i][2] == "user": + self.uninstall_success = False + continue # Skip if app is a user install app + + pk_command.append(fails[i][0]) + try: + print(pk_command) + subprocess.run(pk_command, capture_output=False, check=True) + except subprocess.CalledProcessError: + self.uninstall_success = False + + if should_trash: + host_paks = self.getHostFlatpaks() + host_refs = [] + for i in range(len(host_paks)): + host_refs.append(host_paks[i][8]) + + for i in range(len(apps)): + if apps[i][0] in host_refs: + print(f"{apps[i][1]} is still installed") + else: + self.trashFolder(f"{self.user_data_path}{apps[i][1]}") + + def installFlatpak(self, app_arr, remote, user_or_system): + self.install_success = True + fails = [] + + for i in range(len(app_arr)): + command = ['flatpak-spawn', '--host', 'flatpak', 'install', remote, f"--{user_or_system}", '-y', app_arr[i]] + try: + subprocess.run(command, capture_output=False, check=True) + except subprocess.CalledProcessError: + fails.append(app_arr[i]) + + if (len(fails) > 0) and (user_or_system == "system"): + pk_command = ['flatpak-spawn', '--host', 'pkexec', 'flatpak', 'install', remote, f"--{user_or_system}", '-y'] + for i in range(len(fails)): + pk_command.append(fails[i]) + try: + subprocess.run(pk_command, capture_output=False, check=True) + except subprocess.CalledProcessError: + self.install_success = False + + if (len(fails) > 0) and (user_or_system == "user"): + self.install_success = False \ No newline at end of file diff --git a/src/main.py b/src/main.py index c1003cf..036ff4c 100644 --- a/src/main.py +++ b/src/main.py @@ -28,7 +28,7 @@ gi.require_version("Adw", "1") from gi.repository import Gtk, Gio, Adw, GLib from .window import WarehouseWindow from .remotes import RemotesWindow - +from .orphans_window import OrphansWindow class WarehouseApplication(Adw.Application): """The main application singleton class.""" @@ -42,7 +42,7 @@ class WarehouseApplication(Adw.Application): self.create_action("about", self.on_about_action) self.create_action("preferences", self.on_preferences_action) self.create_action("search", self.on_search_action, ["f"]) - self.create_action("manage-data-folders", self.on_manage_data_folders_action) + self.create_action("manage-data-folders", self.manage_data_shortcut) self.create_action("toggle-batch-mode", self.batch_mode_shortcut, ["b", "Return"]) self.create_action("select-all-in-batch-mode", self.select_all_shortcut, ["a"]) self.create_action("manage-data-folders", self.manage_data_shortcut, ["d"]) @@ -66,7 +66,8 @@ class WarehouseApplication(Adw.Application): self.props.active_window.batch_select_all_handler(select_button) def manage_data_shortcut(self, widget, _): - self.props.active_window.orphans_window() + #self.props.active_window.orphans_window() + OrphansWindow(self.props.active_window).present() def refresh_list_shortcut(self, widget, _): self.props.active_window.refresh_list_of_flatpaks(widget, True) @@ -78,6 +79,10 @@ class WarehouseApplication(Adw.Application): def show_remotes_shortcut(self, widget, _): RemotesWindow(self.props.active_window).present() + # def on_manage_data_folders_action(self, widget, _): + # #self.props.active_window.orphans_window() + # OrphansWindow(self.props.active_window).present() + def do_activate(self): """Called when the application is activated. @@ -115,8 +120,6 @@ class WarehouseApplication(Adw.Application): def on_search_action(self, widget, _): self.props.active_window.search_bar.set_search_mode(not self.props.active_window.search_bar.get_search_mode()) - def on_manage_data_folders_action(self, widget, _): - self.props.active_window.orphans_window() def on_show_runtimes_action(self, widget, _): self.show_runtimes_stateful.set_state(GLib.Variant.new_boolean(state := (not self.show_runtimes_stateful.get_property("state").get_boolean()))) diff --git a/src/meson.build b/src/meson.build index 9bb564d..6ec3c0d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,6 +6,7 @@ blueprints = custom_target('blueprints', input: files( 'gtk/help-overlay.blp', 'window.blp', + 'orphans.blp', ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], @@ -44,6 +45,7 @@ warehouse_sources = [ 'orphans_window.py', 'remotes.py', 'common.py', + 'orphans.blp', ] install_data(warehouse_sources, install_dir: moduledir) diff --git a/src/orphans.blp b/src/orphans.blp new file mode 100644 index 0000000..8b11efe --- /dev/null +++ b/src/orphans.blp @@ -0,0 +1,65 @@ +using Gtk 4.0; +using Adw 1; + +template OrphansWindow : Adw.Window { + default-width: 500; + default-height: 450; + + Adw.ToolbarView main_toolbar_view { + [top] + HeaderBar header_bar { + // [start] + // Button refresh_button { + // icon-name: "view-refresh-symbolic"; + // tooltip-text: _("Refresh the List of Installed Apps"); + // } + } + content: + Adw.ToastOverlay toast_overlay { + Stack main_stack { + Box main_box { + orientation: vertical; + Overlay main_overlay { + ScrolledWindow scrolled_window { + vexpand: true; + Adw.Clamp{ + ListBox list_of_data { + margin-top: 6; + margin-bottom: 6; + margin-start: 12; + margin-end: 12; + hexpand: true; + valign: start; + selection-mode: none; + styles["boxed-list"] + } + } + } + } + } + Adw.StatusPage no_data { + icon-name: "check-plain-symbolic"; + title: _("No Leftover Data"); + description: _("There is no leftover user data"); + } + } + }; + [bottom] + ActionBar action_bar { + [start] + ToggleButton select_all_button { + label: _("Select All"); + } + [end] + Button trash_button { + label: _("Trash"); + sensitive: false; + } + [end] + Button install_button { + label: _("Install"); + sensitive: false; + } + } + } +} \ No newline at end of file diff --git a/src/orphans_window.py b/src/orphans_window.py index 80c4504..70fcc52 100644 --- a/src/orphans_window.py +++ b/src/orphans_window.py @@ -1,6 +1,214 @@ from gi.repository import Gtk, Adw, GLib, Gdk, Gio +from .common import myUtils import subprocess import os +import pathlib -def show_orphans_window(): - pass # place holder until I properly seperate the files \ No newline at end of file +@Gtk.Template(resource_path="/io/github/heliguy4599/Warehouse/orphans.ui") +class OrphansWindow(Adw.Window): + __gtype_name__ = "OrphansWindow" + + list_of_data = Gtk.Template.Child() + install_button = Gtk.Template.Child() + trash_button = Gtk.Template.Child() + select_all_button = Gtk.Template.Child() + main_overlay = Gtk.Template.Child() + toast_overlay = Gtk.Template.Child() + + window_title = _("Manage Leftover Data") + host_home = str(pathlib.Path.home()) + user_data_path = host_home + "/.var/app/" + should_select_all = False + selected_remote = "" + selected_remote_install_type = "" + should_pulse = False + no_close_id = 0 + + def key_handler(self, _a, event, _c, _d): + if event == Gdk.KEY_Escape: + self.close() + + def pulser(self): + if self.should_pulse: + self.progress_bar.pulse() + GLib.timeout_add(500, self.pulser) + + def selectionHandler(self, widget, dir_name): + if widget.get_active(): + self.selected_dirs.append(dir_name) + else: + self.selected_dirs.remove(dir_name) + + if len(self.selected_dirs) == 0: + self.set_title(self.window_title) # Set the window title back to what it was when there are no selected dirs + else: + self.set_title(("{} selected").format(str(len(self.selected_dirs)))) # Set the window title to the amount of selected dirs + + if len(self.selected_dirs) == 0: + self.install_button.set_sensitive(False) + self.trash_button.set_sensitive(False) + else: + self.install_button.set_sensitive(True) + self.trash_button.set_sensitive(True) + + def selectAllHandler(self, button): + self.should_select_all = button.get_active() + self.generateList() + + def installCallback(self, *_args): + self.set_title(self.window_title) + self.generateList() + self.should_pulse = False + self.progress_bar.set_visible(False) + self.set_sensitive(True) + self.app_window.refresh_list_of_flatpaks(self, False) + self.disconnect(self.no_close_id) # Make window able to close + if self.my_utils.install_success: + self.toast_overlay.add_toast(Adw.Toast.new(_("Installed successfully"))) + else: + self.toast_overlay.add_toast(Adw.Toast.new(_("Some apps didn't install"))) + + def installHandler(self): + self.set_title(_("Installing... This could take a while")) + task = Gio.Task.new(None, None, self.installCallback) + task.run_in_thread(lambda _task, _obj, _data, _cancellable, id_list=self.selected_dirs, remote=self.selected_remote, app_type=self.selected_remote_type: self.my_utils.installFlatpak(id_list, remote, app_type)) + + def installButtonHandler(self, button): + remote_select_buttons = [] + self.should_pulse = True + self.pulser() + + def remote_select_handler(button): + if not button.get_active(): + return + remote_index = remote_select_buttons.index(button) + self.selected_remote = self.host_remotes[remote_index][0] + self.selected_remote_type = self.host_remotes[remote_index][7] + + def onResponse(dialog, response_id, _function): + if response_id == "cancel": + self.should_pulse = False + return + self.installHandler() + self.progress_bar.set_visible(True) + self.set_sensitive(False) + self.no_close_id = self.connect("close-request", lambda event: True) # Make window unable to close + + dialog = Adw.MessageDialog.new(self, _("Attempt to Install?"), _("Warehouse will attempt to install apps matching the selected data.")) + dialog.set_close_response("cancel") + dialog.add_response("cancel", _("Cancel")) + dialog.add_response("continue", _("Install")) + dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED) + + height = 65 * len(self.host_remotes) + max = 400 + if height > max: + height = max + remotes_scroll = Gtk.ScrolledWindow(vexpand=True, min_content_height=height) + remote_list = Gtk.ListBox(selection_mode="none", valign="start") + remotes_scroll.set_child(remote_list) + remote_list.add_css_class("boxed-list") + + for i in range(len(self.host_remotes)): + remote_row = Adw.ActionRow(title=self.host_remotes[i][1]) + label = Gtk.Label(label=_("{} wide").format(self.host_remotes[i][7])) + remote_select = Gtk.CheckButton() + remote_select_buttons.append(remote_select) + remote_select.connect("toggled", remote_select_handler) + remote_row.set_activatable_widget(remote_select) + + if remote_row.get_title() == '-': + remote_row.set_title(self.host_remotes[i][0]) + + if i > 0: + remote_select.set_group(remote_select_buttons[i-1]) + + remote_row.add_prefix(remote_select) + remote_row.add_suffix(label) + remote_list.append(remote_row) + + remote_select_buttons[0].set_active(True) + + if len(self.host_remotes) > 1: + dialog.set_extra_child(remotes_scroll) + + dialog.connect("response", onResponse, dialog.choose_finish) + dialog.present() + + def trashHandler(self, button): + + def onResponse(dialog, response_id, _function): + if response_id == "cancel": + return + for i in range(len(self.selected_dirs)): + path = self.user_data_path + self.selected_dirs[i] + self.my_utils.trashFolder(path) + self.select_all_button.set_active(False) + self.generateList() + + dialog = Adw.MessageDialog.new(self, _("Trash folders?"), _("These folders will be moved to the trash.")) + dialog.connect("response", onResponse, dialog.choose_finish) + dialog.set_close_response("cancel") + dialog.add_response("cancel", _("Cancel")) + dialog.add_response("continue", _("Continue")) + dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.present() + + # Create the list of folders in the window + def generateList(self): + self.host_flatpaks = self.my_utils.getHostFlatpaks() + self.list_of_data.remove_all() + self.selected_dirs = [] + self.set_title(self.window_title) + self.should_pulse = False + dir_list = os.listdir(self.user_data_path) + + # This is a list that only holds IDs of install flatpaks + id_list = [] + for i in range(len(self.host_flatpaks)): + id_list.append(self.host_flatpaks[i][2]) + + for i in range(len(dir_list)): + dir_name = dir_list[i] + + # Skip item if it has a matching flatpak + if dir_name in id_list: + continue + + # Create row element + dir_row = Adw.ActionRow(title=dir_name) + dir_row.set_subtitle(self.my_utils.getSizeWithFormat(self.user_data_path + dir_name)) + + select_button = Gtk.CheckButton() + select_button.add_css_class("selection-mode") + select_button.connect("toggled", self.selectionHandler, dir_name) + select_button.set_active(self.should_select_all) + dir_row.add_suffix(select_button) + dir_row.set_activatable_widget(select_button) + + # Add row to list + self.list_of_data.append(dir_row) + + def __init__(self, main_window, **kwargs): + super().__init__(**kwargs) + self.my_utils = myUtils(self) # Access common utils and set the window to this window + self.host_remotes = self.my_utils.getHostRemotes() + self.host_flatpaks = self.my_utils.getHostFlatpaks() + + self.progress_bar = Gtk.ProgressBar(visible=False, pulse_step=0.7) + self.progress_bar.add_css_class("osd") + self.app_window = main_window + + self.set_modal(True) + self.set_transient_for(main_window) + self.set_size_request(260, 230) + self.generateList() + + event_controller = Gtk.EventControllerKey() + event_controller.connect("key-pressed", self.key_handler) + self.add_controller(event_controller) + + self.install_button.connect("clicked", self.installButtonHandler) + self.trash_button.connect("clicked", self.trashHandler) + self.select_all_button.connect("toggled", self.selectAllHandler) + self.main_overlay.add_overlay(self.progress_bar) \ No newline at end of file diff --git a/src/properties_window.py b/src/properties_window.py index 336bf2e..2a0590b 100644 --- a/src/properties_window.py +++ b/src/properties_window.py @@ -1,5 +1,5 @@ from gi.repository import Gtk, Adw, GLib, Gdk, Gio -from .functions import functions +from .common import myUtils import subprocess import os @@ -25,7 +25,7 @@ def show_properties_window(widget, index, window): user_data_list.append(user_data_row) user_data_list.add_css_class("boxed-list") - func = functions(window) + my_utils = myUtils(window) def key_handler(_a, event, _c, _d): if event == Gdk.KEY_Escape: @@ -42,7 +42,7 @@ def show_properties_window(widget, index, window): def on_response(_a, response_id, _b): if response_id != "continue": return - if func.trash_folder(data_folder) == 0: + if my_utils.trashFolder(data_folder) == 0: properties_toast_overlay.add_toast(Adw.Toast.new(_("Trashed user data"))) user_data_list.remove(user_data_row) user_data_list.append(Adw.ActionRow(title="No User Data")) @@ -70,7 +70,7 @@ def show_properties_window(widget, index, window): window.clipboard.set(to_copy) properties_toast_overlay.add_toast(Adw.Toast.new(_("Copied {}").format(title))) - image = func.find_app_icon(window.host_flatpaks[index][2]) + image = my_utils.findAppIcon(window.host_flatpaks[index][2]) image.add_css_class("icon-dropshadow") image.set_margin_top(12) image.set_pixel_size(100) @@ -83,7 +83,7 @@ def show_properties_window(widget, index, window): if os.path.exists(path): user_data_row.set_title("User Data") - user_data_row.set_subtitle(f"{path}\n~{func.get_size_format(func.get_directory_size(path))}") + user_data_row.set_subtitle(f"{path}\n~{my_utils.getSizeWithFormat(path)}") open_button = Gtk.Button(icon_name="document-open-symbolic", valign=Gtk.Align.CENTER, tooltip_text=_("Open Data Folder")) open_button.add_css_class("flat") diff --git a/src/remotes.py b/src/remotes.py index 180deb1..dafd6c3 100644 --- a/src/remotes.py +++ b/src/remotes.py @@ -61,7 +61,7 @@ class RemotesWindow(Adw.Window): def name_update(widget): is_enabled = True self.name_to_add = widget.get_text() - name_pattern = re.compile(r'^[a-zA-Z]+$') + name_pattern = re.compile(r'^[a-zA-Z\-]+$') if not name_pattern.match(self.name_to_add): is_enabled = False @@ -157,15 +157,53 @@ class RemotesWindow(Adw.Window): self.generate_list() def remove_handler(self, _widget, index): + def remove_apps_check_handler(button): + if button.get_active(): + apps_box.prepend(apps_scroll) + apps_box.prepend(label) + else: + apps_box.remove(label) + apps_box.remove(apps_scroll) name = self.host_remotes[index][0] title = self.host_remotes[index][1] install_type = self.host_remotes[index][7] - dialog = Adw.MessageDialog.new(self, _("Remove {}?").format(name), _("Any installed apps from {} will stop receiving updates").format(name)) + + body_text = _("Any installed apps from {} will stop receiving updates").format(name) + dialog = Adw.MessageDialog.new(self, _("Remove {}?").format(name), body_text) dialog.set_close_response("cancel") dialog.add_response("cancel", _("Cancel")) dialog.add_response("continue", _("Remove")) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.connect("response", self.remove_on_response, dialog.choose_finish, index) + + label = Gtk.Label(label=_("These apps will be uninstalled")) + remove_apps = Gtk.CheckButton(label=_("Uninstall apps from this remote")) + remove_apps.connect("toggled", remove_apps_check_handler) + + height = 400 + apps_box = Gtk.Box(orientation="vertical") + apps_scroll = Gtk.ScrolledWindow(vexpand=True, min_content_height=height, margin_top=6, margin_bottom=6) + apps_list = Gtk.ListBox(selection_mode="none", valign="start") + apps_list.add_css_class("boxed-list") + apps_box.append(remove_apps) + #apps_box.append(apps_scroll) + + for i in range(len(self.host_flatpaks)): + if self.host_flatpaks[i][6] != name: + continue + if self.host_flatpaks[i][7] != install_type: + continue + + app_row = Adw.ActionRow(title=self.host_flatpaks[i][0]) + apps_list.append(app_row) + + + + + + + apps_scroll.set_child(apps_list) + dialog.set_extra_child(apps_box) dialog.present() def generate_list(self): diff --git a/src/warehouse.gresource.xml b/src/warehouse.gresource.xml index fe9ddcd..289ad7b 100644 --- a/src/warehouse.gresource.xml +++ b/src/warehouse.gresource.xml @@ -2,6 +2,7 @@ window.ui + orphans.ui gtk/help-overlay.ui diff --git a/src/window.py b/src/window.py index 56affe5..c039286 100644 --- a/src/window.py +++ b/src/window.py @@ -22,7 +22,7 @@ import subprocess from gi.repository import Adw, Gdk, Gio, GLib, Gtk from .properties_window import show_properties_window -from .orphans_window import show_orphans_window +#from .orphans_window import show_orphans_window from .common import myUtils @Gtk.Template(resource_path="/io/github/heliguy4599/Warehouse/window.ui") @@ -56,7 +56,6 @@ class WarehouseWindow(Adw.ApplicationWindow): in_batch_mode = False should_select_all = False host_flatpaks = None - uninstall_success = True install_success = True should_pulse = True no_close = None @@ -76,7 +75,7 @@ class WarehouseWindow(Adw.ApplicationWindow): self.refresh_list_of_flatpaks(_a, False) self.main_toolbar_view.set_sensitive(True) self.disconnect(self.no_close) - if self.uninstall_success: + if self.my_utils.uninstall_success: if self.in_batch_mode: self.toast_overlay.add_toast(Adw.Toast.new(_("Uninstalled selected apps"))) else: @@ -84,40 +83,22 @@ class WarehouseWindow(Adw.ApplicationWindow): else: self.toast_overlay.add_toast(Adw.Toast.new(_("Could not uninstall some apps"))) - def uninstall_flatpak_thread(self, ref_arr, id_arr, should_trash): - failures = [] - for i in range(len(ref_arr)): - try: - subprocess.run(["flatpak-spawn", "--host", "flatpak", "remove", "-y", ref_arr[i]], capture_output=False, check=True) - except subprocess.CalledProcessError: - failures.append(ref_arr[i]) - - if len(failures) > 0: - pk_command = ["flatpak-spawn", "--host", "pkexec", "flatpak", "remove", "-y"] - for i in range(len(failures)): - pk_command.append(failures[i]) - try: - subprocess.run(pk_command, capture_output=False, check=True) - except subprocess.CalledProcessError: - self.uninstall_success = False - - if should_trash: - for i in range(len(id_arr)): - try: - subprocess.run(["flatpak-spawn", "--host", "gio", "trash", f"{self.user_data_path}{id_arr[i]}"]) - except subprocess.CalledProcessError: - self.toast_overlay.add_toast(Adw.Toast.new(_("Could not trash data"))) + def uninstall_flatpak_thread(self, ref_arr, id_arr, type_arr, should_trash): + self.my_utils.uninstallFlatpak(ref_arr, type_arr, should_trash) def uninstall_flatpak(self, index_arr, should_trash): ref_arr = [] id_arr = [] + type_arr = [] for i in range(len(index_arr)): ref = self.host_flatpaks[index_arr[i]][8] id = self.host_flatpaks[index_arr[i]][2] + app_type = self.host_flatpaks[index_arr[i]][7] ref_arr.append(ref) id_arr.append(id) + type_arr.append(app_type) task = Gio.Task.new(None, None, self.uninstall_flatpak_callback) - task.run_in_thread(lambda _task, _obj, _data, _cancellable, ref_arr=ref_arr, id_arr=id_arr, should_trash=should_trash: self.uninstall_flatpak_thread(ref_arr, id_arr, should_trash)) + task.run_in_thread(lambda _task, _obj, _data, _cancellable, ref_arr=ref_arr, id_arr=id_arr, type_arr=type_arr ,should_trash=should_trash: self.uninstall_flatpak_thread(ref_arr, id_arr, type_arr, should_trash)) def batch_uninstall_button_handler(self, _widget): self.should_pulse = True @@ -180,253 +161,6 @@ class WarehouseWindow(Adw.ApplicationWindow): dialog.connect("response", uninstall_response, dialog.choose_finish) Gtk.Window.present(dialog) - def orphans_window(self): - global window_title - window_title = _("Manage Leftover Data") - orphans_window = Adw.Window(title=window_title) - orphans_clamp = Adw.Clamp() - orphans_scroll = Gtk.ScrolledWindow() - orphans_toast_overlay = Adw.ToastOverlay() - orphans_stack = Gtk.Stack() - orphans_overlay = Gtk.Overlay() - orphans_progress_bar = Gtk.ProgressBar(visible=False, pulse_step=0.7) - orphans_toolbar_view = Adw.ToolbarView() - orphans_title_bar = Gtk.HeaderBar() - orphans_action_bar = Gtk.ActionBar() - orphans_list = Gtk.ListBox(selection_mode="none", valign=Gtk.Align.START, margin_top=6, margin_bottom=6, margin_start=12, margin_end=12) - no_data = Adw.StatusPage(icon_name="check-plain-symbolic", title=_("No Data"), description=_("There is no leftover user data")) - - orphans_window.set_default_size(500, 450) - orphans_window.set_size_request(260, 230) - orphans_window.set_modal(True) - orphans_window.set_resizable(True) - orphans_window.set_transient_for(self) - - orphans_stack.add_child(orphans_overlay) - orphans_toast_overlay.set_child(orphans_stack) - orphans_progress_bar.add_css_class("osd") - orphans_overlay.add_overlay(orphans_progress_bar) - orphans_overlay.set_child(orphans_scroll) - - orphans_toolbar_view.add_top_bar(orphans_title_bar) - orphans_toolbar_view.add_bottom_bar(orphans_action_bar) - orphans_toolbar_view.set_content(orphans_toast_overlay) - orphans_window.set_content(orphans_toolbar_view) - orphans_list.add_css_class("boxed-list") - orphans_scroll.set_child(orphans_clamp) - orphans_clamp.set_child(orphans_list) - orphans_stack.add_child(no_data) - - global total_selected - total_selected = 0 - global selected_rows - selected_rows = [] - should_pulse = False - show_orphans_window() - - def orphans_pulser(): - nonlocal should_pulse - if should_pulse: - orphans_progress_bar.pulse() - GLib.timeout_add(500, orphans_pulser) - - def toggle_button_handler(button): - if button.get_active(): - generate_list(button, True) - else: - generate_list(button, False) - - def generate_list(widget, is_select_all): - global window_title - orphans_window.set_title(window_title) - global total_selected - total_selected = 0 - global selected_rows - selected_rows = [] - trash_button.set_sensitive(False) - install_button.set_sensitive(False) - - orphans_list.remove_all() - file_list = os.listdir(self.user_data_path) - id_list = [] - - for i in range(len(self.host_flatpaks)): - id_list.append(self.host_flatpaks[i][2]) - - row_index = -1 - for i in range(len(file_list)): - if not file_list[i] in id_list: - row_index += 1 - select_orphans_tickbox = Gtk.CheckButton(halign=Gtk.Align.CENTER) - orphans_row = Adw.ActionRow(title=GLib.markup_escape_text(file_list[i]), subtitle=_("~") + self.my_utils.getSizeFormat(self.my_utils.getDirectorySize(f"{self.user_data_path}{file_list[i]}"))) - orphans_row.add_suffix(select_orphans_tickbox) - orphans_row.set_activatable_widget(select_orphans_tickbox) - select_orphans_tickbox.connect("toggled", selection_handler, orphans_row.get_title()) - if is_select_all == True: - select_orphans_tickbox.set_active(True) - orphans_list.append(orphans_row) - if not orphans_list.get_row_at_index(0): - orphans_stack.set_visible_child(no_data) - orphans_action_bar.set_revealed(False) - - def key_handler(_a, event, _c, _d): - if event == Gdk.KEY_Escape: - orphans_window.close() - elif event == Gdk.KEY_Delete or event == Gdk.KEY_BackSpace: - trash_button_handler(event) - - def trash_button_handler(widget): - if total_selected == 0: - return 1 - show_success = True - for i in range(len(selected_rows)): - path = f"{self.user_data_path}{selected_rows[i]}" - try: - subprocess.run(["flatpak-spawn", "--host", "gio", "trash", path], capture_output=False, check=True) - except: - orphans_toast_overlay.add_toast(Adw.Toast.new(_("Can't trash {}").format(selected_rows[i]))) - show_success = False - select_all_button.set_active(False) - - if show_success: - orphans_toast_overlay.add_toast(Adw.Toast.new(_("Trashed data"))) - - generate_list(widget, False) - - handler_id = 0 - - def install_callback(*_args): - nonlocal should_pulse - nonlocal handler_id - - if self.install_success: - orphans_toast_overlay.add_toast(Adw.Toast.new(_("Installed all apps"))) - else: - orphans_toast_overlay.add_toast(Adw.Toast.new(_("Some apps didn't install"))) - select_all_button.set_active(False) - orphans_progress_bar.set_visible(False) - should_pulse = False - self.refresh_list_of_flatpaks(None, False) - generate_list(None, False) - nonlocal orphans_toolbar_view - orphans_toolbar_view.set_sensitive(True) - orphans_window.disconnect(handler_id) # Make window able to close - - def thread_func(id_list, remote): - for i in range(len(id_list)): - try: - subprocess.run(["flatpak-spawn", "--host", "flatpak", "install", "-y", remote[0], f"--{remote[1]}", id_list[i]], capture_output=False, check=True) - except subprocess.CalledProcessError: - if remote[1] == "user": - self.install_success = False - continue - try: - subprocess.run(["flatpak-spawn", "--host", "pkexec", "flatpak", "install", "-y", remote[0], f"--{remote[1]}", id_list[i]], capture_output=False, check=True) - except subprocess.CalledProcessError: - self.install_success = False - - def install_on_response(_a, response_id, _b): - nonlocal should_pulse - if response_id == "cancel": - should_pulse = False - orphans_progress_bar.set_visible(False) - return 1 - - orphans_toast_overlay.add_toast(Adw.Toast.new(_("This could take some time"))) - nonlocal orphans_toolbar_view - orphans_toolbar_view.set_sensitive(False) - nonlocal handler_id - handler_id = orphans_window.connect("close-request", lambda event: True) # Make window unable to close - remote = response_id.split("_") - - orphans_progress_bar.set_visible(True) - - task = Gio.Task.new(None, None, install_callback) - task.run_in_thread(lambda _task, _obj, _data, _cancellable, id_list=selected_rows, remote=remote: thread_func(id_list, remote)) - - def install_button_handler(widget): - self.install_success = True - nonlocal should_pulse - should_pulse = True - orphans_pulser() - - def get_host_remotes(): - output = subprocess.run(["flatpak-spawn", "--host", "flatpak", "remotes"], capture_output=True, text=True).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) - return data - - host_remotes = get_host_remotes() - - dialog = Adw.MessageDialog.new(self, _("Choose a Remote")) - dialog.set_close_response("cancel") - dialog.add_response("cancel", _("Cancel")) - dialog.connect("response", install_on_response, dialog.choose_finish) - dialog.set_transient_for(orphans_window) - if len(host_remotes) > 1: - dialog.set_body(_("Choose the Flatpak Remote Repository where attempted app downloads will be from.")) - for i in range(len(host_remotes)): - remote_name = host_remotes[i][0] - remote_option = host_remotes[i][1] - dialog.add_response(f"{remote_name}_{remote_option}", f"{remote_name} {remote_option}") - dialog.set_response_appearance(f"{remote_name}_{remote_option}", Adw.ResponseAppearance.SUGGESTED) - else: - remote_name = host_remotes[0][0] - remote_option = host_remotes[0][1] - dialog.set_heading("Attempt to Install Matching Flatpaks?") - dialog.add_response(f"{remote_name}_{remote_option}", _("Continue")) - Gtk.Window.present(dialog) - - event_controller = Gtk.EventControllerKey() - event_controller.connect("key-pressed", key_handler) - orphans_window.add_controller(event_controller) - - select_all_button = Gtk.ToggleButton(label=_("Select All")) - select_all_button.connect("toggled", toggle_button_handler) - orphans_action_bar.pack_start(select_all_button) - - trash_button = Gtk.Button(label="Trash", valign=Gtk.Align.CENTER, tooltip_text=_("Trash Selected")) - trash_button.add_css_class("destructive-action") - trash_button.connect("clicked", trash_button_handler) - orphans_action_bar.pack_end(trash_button) - - install_button = Gtk.Button(label="Install", valign=Gtk.Align.CENTER, tooltip_text=_("Attempt to Install Selected")) - install_button.connect("clicked", install_button_handler) - install_button.set_visible(False) - orphans_action_bar.pack_end(install_button) - test = subprocess.run(["flatpak-spawn", "--host", "flatpak", "remotes"], capture_output=True, text=True).stdout - for char in test: - if char.isalnum(): - install_button.set_visible(True) - - def selection_handler(tickbox, file): - global total_selected - global selected_rows - if tickbox.get_active() == True: - total_selected += 1 - selected_rows.append(file) - else: - total_selected -= 1 - to_find = file - selected_rows.remove(to_find) - - if total_selected == 0: - orphans_window.set_title(window_title) - trash_button.set_sensitive(False) - install_button.set_sensitive(False) - select_all_button.set_active(False) - else: - orphans_window.set_title(_("{} Selected").format(total_selected)) - trash_button.set_sensitive(True) - install_button.set_sensitive(True) - - generate_list(self, False) - orphans_window.present() - selected_host_flatpak_indexes = [] def generate_list_of_flatpaks(self):