Seperation and improvements

Seperate the orphans window into its own file, seperate non window related functions into a common file, minor UI improvements
This commit is contained in:
heliguy4599
2023-09-22 05:26:21 -04:00
parent f4ab22b49e
commit a1409e870e
9 changed files with 439 additions and 289 deletions

View File

@@ -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
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

View File

@@ -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, ["<primary>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, ["<primary>b", "<primary>Return"])
self.create_action("select-all-in-batch-mode", self.select_all_shortcut, ["<primary>a"])
self.create_action("manage-data-folders", self.manage_data_shortcut, ["<primary>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())))

View File

@@ -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)

65
src/orphans.blp Normal file
View File

@@ -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;
}
}
}
}

View File

@@ -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
@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)

View File

@@ -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")

View File

@@ -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):

View File

@@ -2,6 +2,7 @@
<gresources>
<gresource prefix="/io/github/heliguy4599/Warehouse">
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">orphans.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>
<gresource prefix="/io/github/heliguy4599/Warehouse/icons/scalable/actions/">

View File

@@ -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):