Remove trailing whitespace

This commit is contained in:
Aaron Franke
2024-10-27 20:29:13 -07:00
parent cde215524d
commit da29bafc0f
37 changed files with 626 additions and 632 deletions

View File

@@ -18,4 +18,4 @@ jobs:
uses: flatpak/flatpak-github-actions/flatpak-builder@v6.1 uses: flatpak/flatpak-github-actions/flatpak-builder@v6.1
with: with:
bundle: io.github.flattool.Warehouse.flatpak bundle: io.github.flattool.Warehouse.flatpak
manifest-path: io.github.flattool.Warehouse.json manifest-path: io.github.flattool.Warehouse.json

View File

@@ -672,4 +672,3 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>. <http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -139,7 +139,7 @@
<li>Update to GNOME 46 GTK Technologies</li> <li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Previous Releases's Bug Fixes</p> <p>Previous Releases's Bug Fixes</p>
<ul> <ul>
<li>When an attempt to run an app fails, correct runtime error information is shown</li> <li>When an attempt to run an app fails, correct runtime error information is shown</li>
<li>Install From The Web no longer behaves incorrectly on remote installations with options</li> <li>Install From The Web no longer behaves incorrectly on remote installations with options</li>
@@ -158,7 +158,7 @@
<li>Update to GNOME 46 GTK Technologies</li> <li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>When an attempt to run an app fails, correct runtime error information is shown</li> <li>When an attempt to run an app fails, correct runtime error information is shown</li>
<li>Install From The Web no longer behaves incorrectly on remote installations with options</li> <li>Install From The Web no longer behaves incorrectly on remote installations with options</li>
@@ -168,7 +168,7 @@
</release> </release>
<release version="1.5.1" date="2024-3-8" timestamp="1709921475"> <release version="1.5.1" date="2024-3-8" timestamp="1709921475">
<description translate="no"> <description translate="no">
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Main list is no longer scrolled to the bottom on launch</li> <li>Main list is no longer scrolled to the bottom on launch</li>
<li>Leftover Data window no longer tries to use a different window for toast messages</li> <li>Leftover Data window no longer tries to use a different window for toast messages</li>
@@ -189,7 +189,7 @@
<li>Period, 0 to 9, and underscores are now allowed in new Custom Remote names</li> <li>Period, 0 to 9, and underscores are now allowed in new Custom Remote names</li>
<li>Updated translations</li> <li>Updated translations</li>
</ul> </ul>
<p>Bug Fixes</p> <p>Bug Fixes</p>
<ul> <ul>
<li>Hide Show Disabled Remotes button when there aren't any</li> <li>Hide Show Disabled Remotes button when there aren't any</li>
<li>Fix Batch Snapshots accidentally triggering Select All</li> <li>Fix Batch Snapshots accidentally triggering Select All</li>

View File

@@ -18,21 +18,21 @@ class ChangeVersionPage(Adw.NavigationPage):
versions_group = gtc() versions_group = gtc()
action_bar = gtc() action_bar = gtc()
apply_button = gtc() apply_button = gtc()
selected_commit = None selected_commit = None
failure = None failure = None
def get_commits(self, *args): def get_commits(self, *args):
cmd = ['flatpak-spawn', '--host', 'sh', '-c'] cmd = ['flatpak-spawn', '--host', 'sh', '-c']
script = f"LC_ALL=C flatpak remote-info --log {self.package.info['origin']} {self.package.info['ref']} " script = f"LC_ALL=C flatpak remote-info --log {self.package.info['origin']} {self.package.info['ref']} "
installation = self.package.info["installation"] installation = self.package.info["installation"]
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
script += f"--{installation}" script += f"--{installation}"
else: else:
script += f"--installation={installation}" script += f"--installation={installation}"
cmd.append(script) cmd.append(script)
commits = [] commits = []
changes = [] changes = []
dates = [] dates = []
@@ -55,11 +55,11 @@ class ChangeVersionPage(Adw.NavigationPage):
except Exception as e: except Exception as e:
self.failure = str(e) self.failure = str(e)
return return
if not (len(commits) == len(changes) == len(dates)): if not (len(commits) == len(changes) == len(dates)):
self.failure = "Commits, Changes, and Dates are not of equivalent length" self.failure = "Commits, Changes, and Dates are not of equivalent length"
return return
def idle(*args): def idle(*args):
for index, commit in enumerate(commits): for index, commit in enumerate(commits):
row = Adw.ActionRow(title=GLib.markup_escape_text(changes[index]), subtitle=f"{GLib.markup_escape_text(commit)}\n{GLib.markup_escape_text(dates[index])}") row = Adw.ActionRow(title=GLib.markup_escape_text(changes[index]), subtitle=f"{GLib.markup_escape_text(commit)}\n{GLib.markup_escape_text(dates[index])}")
@@ -73,28 +73,28 @@ class ChangeVersionPage(Adw.NavigationPage):
check.set_group(self.root_group_check_button) check.set_group(self.root_group_check_button)
row.set_activatable_widget(check) row.set_activatable_widget(check)
row.add_prefix(check) row.add_prefix(check)
self.versions_group.add(row) self.versions_group.add(row)
GLib.idle_add(idle) GLib.idle_add(idle)
def set_commit(self, commit): def set_commit(self, commit):
self.selected_commit = commit self.selected_commit = commit
def get_commits_callback(self, *args): def get_commits_callback(self, *args):
if not self.failure is None: if not self.failure is None:
self.toast_overlay.add_toast(ErrorToast(_("Could not get versions"), self.failure).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get versions"), self.failure).toast)
else: else:
self.scrolled_window.set_child(self.versions_clamp) self.scrolled_window.set_child(self.versions_clamp)
def callback(self, did_error): def callback(self, did_error):
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
if not did_error: if not did_error:
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Changed {}'s Version").format(self.package.info['name']))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Changed {}'s Version").format(self.package.info['name'])))
def error_callback(self, user_facing_label, error_message): def error_callback(self, user_facing_label, error_message):
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast) HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)
def on_apply(self, *args): def on_apply(self, *args):
if ChangeVersionWorker.change_version( if ChangeVersionWorker.change_version(
self.mask_row.get_active(), self.mask_row.get_active(),
@@ -104,21 +104,21 @@ class ChangeVersionPage(Adw.NavigationPage):
self.error_callback, self.error_callback,
): ):
self.packages_page.set_status(self.packages_page.changing_version) self.packages_page.set_status(self.packages_page.changing_version)
def __init__(self, packages_page, package, **kwargs): def __init__(self, packages_page, package, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.packages_page = packages_page self.packages_page = packages_page
self.package = package self.package = package
# Apply # Apply
pkg_name = package.info["name"] pkg_name = package.info["name"]
self.set_title(_("{} Versions").format(pkg_name)) self.set_title(_("{} Versions").format(pkg_name))
self.mask_row.set_subtitle(_("Ensure that {} will never be updated to a newer version").format(pkg_name)) self.mask_row.set_subtitle(_("Ensure that {} will never be updated to a newer version").format(pkg_name))
self.scrolled_window.set_child(LoadingStatus(_("Fetching Releases"), _("This could take a while"))) self.scrolled_window.set_child(LoadingStatus(_("Fetching Releases"), _("This could take a while")))
Gio.Task.new(None, None, self.get_commits_callback).run_in_thread(self.get_commits) Gio.Task.new(None, None, self.get_commits_callback).run_in_thread(self.get_commits)
# Connections # Connections
self.root_group_check_button.connect("toggled", lambda *_: self.action_bar.set_revealed(True)) self.root_group_check_button.connect("toggled", lambda *_: self.action_bar.set_revealed(True))
self.apply_button.connect("clicked", self.on_apply) self.apply_button.connect("clicked", self.on_apply)

View File

@@ -8,41 +8,41 @@ class ChangeVersionWorker:
error_callback = None error_callback = None
loading_status = None loading_status = None
did_error = False did_error = False
@classmethod @classmethod
def update_status(this, package_ratio, complete, total): def update_status(this, package_ratio, complete, total):
final_ratio = (package_ratio + complete) / (total or 1) final_ratio = (package_ratio + complete) / (total or 1)
print(f"fr: {final_ratio:.2f}") print(f"fr: {final_ratio:.2f}")
print("r:", package_ratio, ", c:", complete, ", t:", total) print("r:", package_ratio, ", c:", complete, ", t:", total)
print("=======================================") print("=======================================")
if not this.loading_status is None: if not this.loading_status is None:
GLib.idle_add(lambda *_: this.loading_status.progress_bar.set_fraction(final_ratio)) GLib.idle_add(lambda *_: this.loading_status.progress_bar.set_fraction(final_ratio))
@classmethod @classmethod
def change_version_thread(this, should_mask, package, commit): def change_version_thread(this, should_mask, package, commit):
try: try:
cmd = ['flatpak-spawn', '--host', 'pkexec', 'sh', '-c'] cmd = ['flatpak-spawn', '--host', 'pkexec', 'sh', '-c']
installation = package.info['installation'] installation = package.info['installation']
real_installation = "" real_installation = ""
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
real_installation = f"--{installation}" real_installation = f"--{installation}"
else: else:
real_installation = f"--installation={installation}" real_installation = f"--installation={installation}"
suffix = "" suffix = ""
unmask_cmd = f"flatpak mask --remove {real_installation} {package.info['id']} && " unmask_cmd = f"flatpak mask --remove {real_installation} {package.info['id']} && "
change_version_cmd = f"flatpak update {package.info['ref']} --commit={commit} {real_installation} -y" change_version_cmd = f"flatpak update {package.info['ref']} --commit={commit} {real_installation} -y"
mask_cmd = f" && flatpak mask {real_installation} {package.info['id']}" mask_cmd = f" && flatpak mask {real_installation} {package.info['id']}"
if package.is_masked: if package.is_masked:
suffix += unmask_cmd suffix += unmask_cmd
suffix += change_version_cmd suffix += change_version_cmd
if should_mask: if should_mask:
suffix += mask_cmd suffix += mask_cmd
cmd.append(suffix) cmd.append(suffix)
this.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) this.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
percent_pattern = r'\d{1,3}%' percent_pattern = r'\d{1,3}%'
@@ -60,52 +60,52 @@ class ChangeVersionWorker:
this.update_status(ratio, complete, total) this.update_status(ratio, complete, total)
else: else:
this.update_status(ratio, 0, 1) this.update_status(ratio, 0, 1)
this.process.wait(timeout=10) this.process.wait(timeout=10)
if error := this.process.communicate()[1].strip(): if error := this.process.communicate()[1].strip():
this.on_error(_("Error occurred while changing version"), error) this.on_error(_("Error occurred while changing version"), error)
except subprocess.TimeoutExpired as te: except subprocess.TimeoutExpired as te:
this.process.terminate() this.process.terminate()
this.on_error(_("Error occurred while changing version"), _("Failed to exit cleanly")) this.on_error(_("Error occurred while changing version"), _("Failed to exit cleanly"))
except Exception as e: except Exception as e:
this.process.terminate() this.process.terminate()
this.on_error(_("Error occurred while changing version"), str(e)) this.on_error(_("Error occurred while changing version"), str(e))
@classmethod @classmethod
def cancel(this): def cancel(this):
if this.process is None: if this.process is None:
return return
try: try:
this.process.terminate() this.process.terminate()
this.process.wait(timeout=10) this.process.wait(timeout=10)
except Exception as e: except Exception as e:
this.on_error(_("Could not cancel version change"), str(e)) this.on_error(_("Could not cancel version change"), str(e))
@classmethod @classmethod
def on_done(this, *args): def on_done(this, *args):
this.process = None this.process = None
HostInfo.main_window.remove_refresh_lockout("changing version") HostInfo.main_window.remove_refresh_lockout("changing version")
if not this.loading_status is None: if not this.loading_status is None:
this.loading_status.progress_bar.set_fraction(0.0) this.loading_status.progress_bar.set_fraction(0.0)
if not this.callback is None: if not this.callback is None:
this.callback(this.did_error) this.callback(this.did_error)
@classmethod @classmethod
def on_error(this, user_facing_label, error_message): def on_error(this, user_facing_label, error_message):
this.did_error = True this.did_error = True
if not this.error_callback is None: if not this.error_callback is None:
this.error_callback(user_facing_label, error_message) this.error_callback(user_facing_label, error_message)
@classmethod @classmethod
def change_version(this, should_mask, package, commit, loading_status=None, callback=None, error_callback=None): def change_version(this, should_mask, package, commit, loading_status=None, callback=None, error_callback=None):
if not this.process is None: if not this.process is None:
this.on_error(_("Could not change version"), _("Another package is changing version.")) this.on_error(_("Could not change version"), _("Another package is changing version."))
return False return False
this.loading_status = loading_status this.loading_status = loading_status
this.callback = callback this.callback = callback
this.error_callback = error_callback this.error_callback = error_callback

View File

@@ -6,15 +6,15 @@ from .error_toast import ErrorToast
class AttemptInstallDialog(Adw.AlertDialog): class AttemptInstallDialog(Adw.AlertDialog):
__gtype_name__ = "AttemptInstallDialog" __gtype_name__ = "AttemptInstallDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
preferences_group = gtc() preferences_group = gtc()
def generate_list(self): def generate_list(self):
for installation, remotes in HostInfo.remotes.items(): for installation, remotes in HostInfo.remotes.items():
for remote in remotes: for remote in remotes:
if remote.disabled: if remote.disabled:
continue continue
row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation)) row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation))
row.remote_name = remote.name row.remote_name = remote.name
row.remote_installation = installation row.remote_installation = installation
@@ -28,20 +28,20 @@ class AttemptInstallDialog(Adw.AlertDialog):
button.set_group(self.rows[0].check_button) button.set_group(self.rows[0].check_button)
else: else:
button.activate() button.activate()
def on_response(self, dialog, response): def on_response(self, dialog, response):
if response != "continue": if response != "continue":
if not self.callback is None: if not self.callback is None:
self.callback(False) self.callback(False)
return return
active_row = None active_row = None
for row in self.rows: for row in self.rows:
if row.check_button.get_active(): if row.check_button.get_active():
active_row = row active_row = row
break break
if not active_row is None: if not active_row is None:
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
HostInfo.main_window.activate_row(HostInfo.main_window.install_row) HostInfo.main_window.activate_row(HostInfo.main_window.install_row)
@@ -54,23 +54,23 @@ class AttemptInstallDialog(Adw.AlertDialog):
}]) }])
elif not self.callback is None: elif not self.callback is None:
self.callback(False) self.callback(False)
def __init__(self, package_names, callback=None, **kwargs): def __init__(self, package_names, callback=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.rows = [] self.rows = []
self.package_names = package_names self.package_names = package_names
self.callback = callback self.callback = callback
# Apply # Apply
self.generate_list() self.generate_list()
if len(self.rows) == 1: if len(self.rows) == 1:
self.set_extra_child(None) self.set_extra_child(None)
elif len(self.rows) < 1: elif len(self.rows) < 1:
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Can't find matching packages"), _("Your system has no remotes added")).toast) HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Can't find matching packages"), _("Your system has no remotes added")).toast)
self.present(HostInfo.main_window) self.present(HostInfo.main_window)
# Connections # Connections
self.connect("response", self.on_response) self.connect("response", self.on_response)

View File

@@ -3,7 +3,6 @@ from gi.repository import Adw, Gtk, Gdk, GLib
class ErrorToast: class ErrorToast:
main_window = None main_window = None
def __init__(self, display_msg, error_msg): def __init__(self, display_msg, error_msg):
def on_response(dialog, response_id): def on_response(dialog, response_id):
if response_id == "copy": if response_id == "copy":
self.clipboard.set(error_msg) self.clipboard.set(error_msg)

View File

@@ -5,7 +5,7 @@ from .host_info import HostInfo
class InstallationChooser(Adw.PreferencesGroup): class InstallationChooser(Adw.PreferencesGroup):
__gtype_name__ = 'InstallationChooser' __gtype_name__ = 'InstallationChooser'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
user_row = gtc() user_row = gtc()
system_row = gtc() system_row = gtc()
single_row = gtc() single_row = gtc()
@@ -14,14 +14,14 @@ class InstallationChooser(Adw.PreferencesGroup):
system_check = gtc() system_check = gtc()
single_check = gtc() single_check = gtc()
choice_check = gtc() choice_check = gtc()
def get_installation(self): def get_installation(self):
for button, func in self.check_buttons.items(): for button, func in self.check_buttons.items():
if button.get_active(): if button.get_active():
return func() return func()
return "" # Case for when no button is active (which shouldn't happen) return "" # Case for when no button is active (which shouldn't happen)
def set_content_strings(self, content_name, is_plural): def set_content_strings(self, content_name, is_plural):
if is_plural: if is_plural:
self.user_row.set_subtitle(_("These {} will only be available to you").format(content_name)) self.user_row.set_subtitle(_("These {} will only be available to you").format(content_name))
@@ -31,31 +31,31 @@ class InstallationChooser(Adw.PreferencesGroup):
self.user_row.set_subtitle(_("This {} will only be available to you").format(content_name)) self.user_row.set_subtitle(_("This {} will only be available to you").format(content_name))
self.system_row.set_subtitle(_("This {} will be available to everyone").format(content_name)) self.system_row.set_subtitle(_("This {} will be available to everyone").format(content_name))
self.set_description(_("Choose how this {} will be installed").format(content_name)) self.set_description(_("Choose how this {} will be installed").format(content_name))
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.check_buttons = { self.check_buttons = {
self.user_check : lambda: "user", self.user_check : lambda: "user",
self.system_check: lambda: "system", self.system_check: lambda: "system",
self.single_check: self.single_row.get_title, self.single_check: self.single_row.get_title,
self.choice_check: lambda: self.choice_row.get_selected_item().get_string(), self.choice_check: lambda: self.choice_row.get_selected_item().get_string(),
} }
# Apply # Apply
custom_installations = [] custom_installations = []
for installation in HostInfo.installations: for installation in HostInfo.installations:
if installation.startswith("user") or installation.startswith("system"): if installation.startswith("user") or installation.startswith("system"):
continue continue
custom_installations.append(installation) custom_installations.append(installation)
if len(custom_installations) == 1: if len(custom_installations) == 1:
self.single_row.set_visible(True) self.single_row.set_visible(True)
self.single_row.set_title(custom_installations[0]) self.single_row.set_title(custom_installations[0])
elif len(custom_installations) > 1: elif len(custom_installations) > 1:
self.choice_row.set_visible(True) self.choice_row.set_visible(True)
self.choice_row.set_model(Gtk.StringList(strings=custom_installations)) self.choice_row.set_model(Gtk.StringList(strings=custom_installations))
# Connections # Connections
self.choice_row.connect("notify::css-classes", lambda *_: self.choice_check.activate()) self.choice_row.connect("notify::css-classes", lambda *_: self.choice_check.activate())

View File

@@ -6,7 +6,7 @@ class SidebarButton(Gtk.Button):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
main_split = HostInfo.main_window.main_split main_split = HostInfo.main_window.main_split

View File

@@ -8,7 +8,6 @@ icon_theme.add_search_path(f"{home}/.local/share/flatpak/exports/share/icons")
direction = Gtk.Image().get_direction() direction = Gtk.Image().get_direction()
class Flatpak: class Flatpak:
def open_app(self, callback=None): def open_app(self, callback=None):
self.failed_app_run = None self.failed_app_run = None
def thread(*args): def thread(*args):
@@ -58,10 +57,10 @@ class Flatpak:
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
if not should_mask: if not should_mask:
cmd.append("--remove") cmd.append("--remove")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
self.is_masked = should_mask self.is_masked = should_mask
@@ -76,7 +75,7 @@ class Flatpak:
self.failed_pin = None self.failed_pin = None
if not self.is_runtime: if not self.is_runtime:
self.failed_pin = "Cannot pin an application" self.failed_pin = "Cannot pin an application"
def thread(*args): def thread(*args):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'pin', f"runtime/{self.info['ref']}"] cmd = ['flatpak-spawn', '--host', 'flatpak', 'pin', f"runtime/{self.info['ref']}"]
installation = self.info["installation"] installation = self.info["installation"]
@@ -99,7 +98,7 @@ class Flatpak:
def uninstall(self, callee_callback=None): def uninstall(self, callee_callback=None):
self.failed_uninstall = None self.failed_uninstall = None
def callback(*args): def callback(*args):
HostInfo.main_window.remove_refresh_lockout("uninstalling packages") HostInfo.main_window.remove_refresh_lockout("uninstalling packages")
if not callee_callback is None: if not callee_callback is None:
@@ -149,16 +148,16 @@ class Flatpak:
first = lines.pop(0) first = lines.pop(0)
if " - " in first: if " - " in first:
cli_info["description"] = first.split(" - ")[1] cli_info["description"] = first.split(" - ")[1]
# Handle descriptions that contain newlines # Handle descriptions that contain newlines
while (line := lines.pop(0)) and not ":" in line: while (line := lines.pop(0)) and not ":" in line:
if len(line) > 0: if len(line) > 0:
cli_info["description"] += f" {line}" cli_info["description"] += f" {line}"
for i, word in enumerate(lines): for i, word in enumerate(lines):
if not ":" in word: if not ":" in word:
continue continue
word = word.strip().split(": ", 1) word = word.strip().split(": ", 1)
if len(word) < 2: if len(word) < 2:
continue continue
@@ -248,7 +247,7 @@ class HostInfo:
lines = output.strip().split("\n") lines = output.strip().split("\n")
for i in lines: for i in lines:
icon_theme.add_search_path(f"{i}/exports/share/icons") icon_theme.add_search_path(f"{i}/exports/share/icons")
flatpaks = [] flatpaks = []
id_to_flatpak = {} id_to_flatpak = {}
ref_to_flatpak = {} ref_to_flatpak = {}
@@ -270,7 +269,6 @@ class HostInfo:
this.dependent_runtime_refs.clear() this.dependent_runtime_refs.clear()
def thread(task, *args): def thread(task, *args):
# Remotes # Remotes
def remote_info(installation): def remote_info(installation):
cmd = ['flatpak-spawn', '--host', cmd = ['flatpak-spawn', '--host',
@@ -353,7 +351,7 @@ class HostInfo:
this.flatpaks.append(package) this.flatpaks.append(package)
this.id_to_flatpak[package.info["id"]] = package this.id_to_flatpak[package.info["id"]] = package
this.ref_to_flatpak[package.info["ref"]] = package this.ref_to_flatpak[package.info["ref"]] = package
# Dependent Runtimes # Dependent Runtimes
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', ['flatpak-spawn', '--host',
@@ -366,19 +364,19 @@ class HostInfo:
split_line = line.split("\t") split_line = line.split("\t")
if len(split_line) < 2 or split_line[0] == '': if len(split_line) < 2 or split_line[0] == '':
continue continue
package = this.flatpaks[index] package = this.flatpaks[index]
if package.is_runtime: if package.is_runtime:
continue continue
runtime = split_line[0] runtime = split_line[0]
package.dependent_runtime = this.ref_to_flatpak[runtime] package.dependent_runtime = this.ref_to_flatpak[runtime]
if not runtime in this.dependent_runtime_refs: if not runtime in this.dependent_runtime_refs:
this.dependent_runtime_refs.append(runtime) this.dependent_runtime_refs.append(runtime)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), cpe.stderr).toast) this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), cpe.stderr).toast)
except Exception as e: except Exception as e:
this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), str(e)).toast) this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), str(e)).toast)
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)

View File

@@ -4,30 +4,30 @@ from gi.repository import Adw, Gtk
class FileInstallDialog(Adw.Dialog): class FileInstallDialog(Adw.Dialog):
__gtype_name__ = "FileInstallDialog" __gtype_name__ = "FileInstallDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
packages_group = gtc() packages_group = gtc()
installation_chooser = gtc() installation_chooser = gtc()
cancel_button = gtc() cancel_button = gtc()
apply_button = gtc() apply_button = gtc()
def generate_list(self): def generate_list(self):
for file in self.files: for file in self.files:
row = Adw.ActionRow(title=file.get_basename()) row = Adw.ActionRow(title=file.get_basename())
row.add_prefix(Gtk.Image(icon_name="flatpak-symbolic")) row.add_prefix(Gtk.Image(icon_name="flatpak-symbolic"))
self.packages_group.add(row) self.packages_group.add(row)
def on_response(self, *args): def on_response(self, *args):
self.on_add(self.installation_chooser.get_installation(), self.files) self.on_add(self.installation_chooser.get_installation(), self.files)
self.close() self.close()
def __init__(self, parent_page, files, on_add, **kwargs): def __init__(self, parent_page, files, on_add, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.parent_page = parent_page self.parent_page = parent_page
self.files = files self.files = files
self.on_add = on_add self.on_add = on_add
# Apply # Apply
self.generate_list() self.generate_list()
if len(files) > 1: if len(files) > 1:
@@ -40,7 +40,7 @@ class FileInstallDialog(Adw.Dialog):
# self.packages_group.set_title(_("Review Package")) # self.packages_group.set_title(_("Review Package"))
self.packages_group.set_description(_("The following package will be installed")) self.packages_group.set_description(_("The following package will be installed"))
self.installation_chooser.set_content_strings(_("package"), False) self.installation_chooser.set_content_strings(_("package"), False)
# Connections # Connections
self.cancel_button.connect("clicked", lambda *_: self.close()) self.cancel_button.connect("clicked", lambda *_: self.close())
self.apply_button.connect("clicked", self.on_response) self.apply_button.connect("clicked", self.on_response)

View File

@@ -11,7 +11,7 @@ from .error_toast import ErrorToast
class InstallPage(Adw.BreakpointBin): class InstallPage(Adw.BreakpointBin):
__gtype_name__ = "InstallPage" __gtype_name__ = "InstallPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
break_point = gtc() break_point = gtc()
split_view = gtc() split_view = gtc()
multi_view = gtc() multi_view = gtc()
@@ -23,42 +23,42 @@ class InstallPage(Adw.BreakpointBin):
bottom_sheet = gtc() bottom_sheet = gtc()
bottom_child = gtc() bottom_child = gtc()
bottom_label = gtc() bottom_label = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
instance = None instance = None
page_name = "install" page_name = "install"
current_installation = "" current_installation = ""
current_remote = None current_remote = None
did_error = False did_error = False
def start_loading(self): def start_loading(self):
self.total_added_packages = 0 self.total_added_packages = 0
self.bottom_bar_visual_handler(False) self.bottom_bar_visual_handler(False)
self.status_stack.set_visible_child(self.loading_view) self.status_stack.set_visible_child(self.loading_view)
self.select_page.start_loading() self.select_page.start_loading()
self.pending_page.reset() self.pending_page.reset()
def end_loading(self): def end_loading(self):
self.select_page.end_loading() self.select_page.end_loading()
self.status_stack.set_visible_child(self.multi_view) self.status_stack.set_visible_child(self.multi_view)
def install_callback(self): def install_callback(self):
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
if not self.did_error: if not self.did_error:
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Installed Packages"))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Installed Packages")))
def install_error_callback(self, user_facing_label, error_message): def install_error_callback(self, user_facing_label, error_message):
self.did_error = True self.did_error = True
GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)) GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast))
def install_packages(self, package_requests): def install_packages(self, package_requests):
self.did_error = False self.did_error = False
if PackageInstallWorker.install(package_requests, self.installing_status, self.install_callback, self.install_error_callback): if PackageInstallWorker.install(package_requests, self.installing_status, self.install_callback, self.install_error_callback):
self.status_stack.set_visible_child(self.installing_view) self.status_stack.set_visible_child(self.installing_view)
def bottom_bar_visual_handler(self, is_added): def bottom_bar_visual_handler(self, is_added):
total = self.total_added_packages total = self.total_added_packages
if total == 0: if total == 0:
@@ -71,25 +71,25 @@ class InstallPage(Adw.BreakpointBin):
self.bottom_child.set_reveal_child(True) self.bottom_child.set_reveal_child(True)
else: else:
self.bottom_label.set_label(_("{} Pending Packages").format(total)) self.bottom_label.set_label(_("{} Pending Packages").format(total))
def package_added(self): def package_added(self):
self.total_added_packages += 1 self.total_added_packages += 1
self.bottom_bar_visual_handler(True) self.bottom_bar_visual_handler(True)
def package_removed(self): def package_removed(self):
self.total_added_packages -= 1 self.total_added_packages -= 1
self.bottom_bar_visual_handler(False) self.bottom_bar_visual_handler(False)
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.instance = self self.instance = self
# Extra Object Creation # Extra Object Creation
self.installing_status = LoadingStatus(_("Installing Packages"), _("This could take a while"), True, PackageInstallWorker.cancel) self.installing_status = LoadingStatus(_("Installing Packages"), _("This could take a while"), True, PackageInstallWorker.cancel)
self.total_added_packages = 0 self.total_added_packages = 0
# Connections # Connections
# Apply # Apply
self.select_page.results_page.pending_page = self.pending_page self.select_page.results_page.pending_page = self.pending_page
self.select_page.results_page.install_page = self self.select_page.results_page.install_page = self

View File

@@ -4,30 +4,30 @@ from .result_row import ResultRow
class AddedGroup(Adw.PreferencesGroup): class AddedGroup(Adw.PreferencesGroup):
__gtype_name__ = "AddedGroup" __gtype_name__ = "AddedGroup"
def add_row(self, row): def add_row(self, row):
self.rows.append(row) self.rows.append(row)
self.add(row) self.add(row)
def rem_row(self, row): def rem_row(self, row):
if row in self.rows: if row in self.rows:
self.rows.remove(row) self.rows.remove(row)
self.remove(row) self.remove(row)
def remove_all(self, *args): def remove_all(self, *args):
while len(self.rows) > 0 and (row := self.rows[0]): while len(self.rows) > 0 and (row := self.rows[0]):
row.activate() row.activate()
def __init__(self, remote, installation, **kwargs): def __init__(self, remote, installation, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.remote = remote self.remote = remote
self.installation = installation self.installation = installation
self.rows = [] self.rows = []
self.set_title(f"{remote.title}") self.set_title(f"{remote.title}")
self.set_description(_("Installation: {}").format(installation)) self.set_description(_("Installation: {}").format(installation))
remove_all = Gtk.Button( remove_all = Gtk.Button(
child=Adw.ButtonContent( child=Adw.ButtonContent(
icon_name="list-remove-all-symbolic", icon_name="list-remove-all-symbolic",
@@ -38,18 +38,18 @@ class AddedGroup(Adw.PreferencesGroup):
remove_all.add_css_class("flat") remove_all.add_css_class("flat")
remove_all.connect("clicked", self.remove_all) remove_all.connect("clicked", self.remove_all)
self.set_header_suffix(remove_all) self.set_header_suffix(remove_all)
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/pending_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/install_page/pending_page.ui")
class PendingPage(Adw.NavigationPage): class PendingPage(Adw.NavigationPage):
__gtype_name__ = "PendingPage" __gtype_name__ = "PendingPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
stack = gtc() stack = gtc()
main_view = gtc() main_view = gtc()
none_pending = gtc() none_pending = gtc()
preferences_page = gtc() preferences_page = gtc()
install_button = gtc() install_button = gtc()
def add_package_row(self, row): def add_package_row(self, row):
self.added_packages.append(row.package) self.added_packages.append(row.package)
row.set_state(ResultRow.PackageState.SELECTED) row.set_state(ResultRow.PackageState.SELECTED)
@@ -64,32 +64,32 @@ class PendingPage(Adw.NavigationPage):
group.add_row(added_row) group.add_row(added_row)
self.groups[key] = group self.groups[key] = group
self.preferences_page.add(group) self.preferences_page.add(group)
added_row.connect("activated", self.remove_package_row, group) added_row.connect("activated", self.remove_package_row, group)
self.stack.set_visible_child(self.main_view) self.stack.set_visible_child(self.main_view)
def remove_package_row(self, row, group): def remove_package_row(self, row, group):
# row.origin_row.set_state(ResultRow.PackageState.NEW) # row.origin_row.set_state(ResultRow.PackageState.NEW)
for item in row.origin_list_box: for item in row.origin_list_box:
if item.state == ResultRow.PackageState.SELECTED and item.package.is_similar(row.package): if item.state == ResultRow.PackageState.SELECTED and item.package.is_similar(row.package):
item.set_state(ResultRow.PackageState.NEW) item.set_state(ResultRow.PackageState.NEW)
break break
group.rem_row(row) group.rem_row(row)
if row.package in self.added_packages: if row.package in self.added_packages:
self.added_packages.remove(row.package) self.added_packages.remove(row.package)
if len(group.rows) == 0: if len(group.rows) == 0:
key = f"{row.package.remote}<>{row.package.installation}" key = f"{row.package.remote}<>{row.package.installation}"
self.groups.pop(key, None) self.groups.pop(key, None)
self.preferences_page.remove(group) self.preferences_page.remove(group)
if len(self.added_packages) == 0: if len(self.added_packages) == 0:
self.stack.set_visible_child(self.none_pending) self.stack.set_visible_child(self.none_pending)
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
install_page.package_removed() install_page.package_removed()
def on_install(self, *args): def on_install(self, *args):
package_requests = [] package_requests = []
for key, group in self.groups.items(): for key, group in self.groups.items():
@@ -101,28 +101,28 @@ class PendingPage(Adw.NavigationPage):
} }
for row in group.rows: for row in group.rows:
item['package_names'].append(row.package.app_id) item['package_names'].append(row.package.app_id)
package_requests.append(item) package_requests.append(item)
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
install_page.install_packages(package_requests) install_page.install_packages(package_requests)
def reset(self): def reset(self):
for key, group in self.groups.items(): for key, group in self.groups.items():
self.preferences_page.remove(group) self.preferences_page.remove(group)
self.groups.clear() self.groups.clear()
self.added_packages.clear() self.added_packages.clear()
self.stack.set_visible_child(self.none_pending) self.stack.set_visible_child(self.none_pending)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.groups = {} # remote<>installation: adw.preference_group self.groups = {} # remote<>installation: adw.preference_group
self.added_packages = [] self.added_packages = []
# Connections # Connections
self.install_button.connect("clicked", self.on_install) self.install_button.connect("clicked", self.on_install)
# Apply # Apply

View File

@@ -35,7 +35,7 @@ class AddedPackage:
class ResultsPage(Adw.NavigationPage): class ResultsPage(Adw.NavigationPage):
__gtype_name__ = "ResultsPage" __gtype_name__ = "ResultsPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
search_entry = gtc() search_entry = gtc()
results_list = gtc() results_list = gtc()
stack = gtc() stack = gtc()
@@ -43,7 +43,7 @@ class ResultsPage(Adw.NavigationPage):
too_many = gtc() too_many = gtc()
results_view= gtc() results_view= gtc()
no_results = gtc() no_results = gtc()
def show_remote(self, row, remote, installation, nav_view=None): def show_remote(self, row, remote, installation, nav_view=None):
self.remote = remote self.remote = remote
self.installation = installation self.installation = installation
@@ -52,12 +52,12 @@ class ResultsPage(Adw.NavigationPage):
self.search_entry.grab_focus() self.search_entry.grab_focus()
if nav_view: if nav_view:
nav_view.push(self) nav_view.push(self)
def add_package_row(self, row): def add_package_row(self, row):
self.pending_page.add_package_row(row) self.pending_page.add_package_row(row)
if not self.install_page is None: if not self.install_page is None:
self.install_page.package_added() self.install_page.package_added()
def on_search(self, *args): def on_search(self, *args):
self.packages.clear() self.packages.clear()
self.stack.set_visible_child(self.loading) self.stack.set_visible_child(self.loading)
@@ -66,14 +66,14 @@ class ResultsPage(Adw.NavigationPage):
if search_text == "": if search_text == "":
self.stack.set_visible_child(self.new_search) self.stack.set_visible_child(self.new_search)
return return
def thread(*args): def thread(*args):
installation = "" installation = ""
if self.installation == "user" or self.installation == "system": if self.installation == "user" or self.installation == "system":
installation = f"--{self.installation}" installation = f"--{self.installation}"
else: else:
installation = f"--installation={self.installation}" installation = f"--installation={self.installation}"
try: try:
output = subprocess.run( output = subprocess.run(
['flatpak-spawn', '--host', 'flatpak', 'search', '--columns=all', installation, self.search_entry.get_text()], ['flatpak-spawn', '--host', 'flatpak', 'search', '--columns=all', installation, self.search_entry.get_text()],
@@ -82,51 +82,51 @@ class ResultsPage(Adw.NavigationPage):
if len(output) > 100: if len(output) > 100:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.too_many)) GLib.idle_add(lambda *_: self.stack.set_visible_child(self.too_many))
return return
for line in output: for line in output:
line = line.strip() line = line.strip()
info = line.split('\t') info = line.split('\t')
if len(info) != 6: if len(info) != 6:
continue continue
remotes = info[5].split(',') remotes = info[5].split(',')
if not self.remote.name in remotes: if not self.remote.name in remotes:
continue continue
package = AddedPackage(info[0], info[2], info[4], info[3], self.remote, self.installation) package = AddedPackage(info[0], info[2], info[4], info[3], self.remote, self.installation)
row = ResultRow(package, ResultRow.PackageState.NEW, self.results_list) row = ResultRow(package, ResultRow.PackageState.NEW, self.results_list)
for item in self.pending_page.added_packages: for item in self.pending_page.added_packages:
if package.is_similar(item): if package.is_similar(item):
row.set_state(ResultRow.PackageState.SELECTED) row.set_state(ResultRow.PackageState.SELECTED)
if package.app_id in HostInfo.id_to_flatpak: if package.app_id in HostInfo.id_to_flatpak:
installed_package = HostInfo.id_to_flatpak[package.app_id] installed_package = HostInfo.id_to_flatpak[package.app_id]
if installed_package.info["id"] == package.app_id and installed_package.info["branch"] == package.branch: if installed_package.info["id"] == package.app_id and installed_package.info["branch"] == package.branch:
row.set_state(ResultRow.PackageState.INSTALLED) row.set_state(ResultRow.PackageState.INSTALLED)
row.connect("activated", self.add_package_row) row.connect("activated", self.add_package_row)
self.packages.append(package) self.packages.append(package)
GLib.idle_add(lambda *_, _row=row: self.results_list.append(_row)) GLib.idle_add(lambda *_, _row=row: self.results_list.append(_row))
if len(self.packages) > 0: if len(self.packages) > 0:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.results_view)) GLib.idle_add(lambda *_: self.stack.set_visible_child(self.results_view))
else: else:
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.no_results)) GLib.idle_add(lambda *_: self.stack.set_visible_child(self.no_results))
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
GLib.idle_add(lambda *_, cpe=cpe: HostInfo.main_window.toast_overlay.add_toast(ErrorToast("Could not search for package", cpe.stderr).toast)) GLib.idle_add(lambda *_, cpe=cpe: HostInfo.main_window.toast_overlay.add_toast(ErrorToast("Could not search for package", cpe.stderr).toast))
GLib.idle_add(lambda *_: self.install_page.select_page.nav_view.pop()) GLib.idle_add(lambda *_: self.install_page.select_page.nav_view.pop())
Gio.Task().run_in_thread(thread) Gio.Task().run_in_thread(thread)
def on_back(self, *args): def on_back(self, *args):
self.results_list.remove_all() self.results_list.remove_all()
self.stack.set_visible_child(self.new_search) self.stack.set_visible_child(self.new_search)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.remote = None self.remote = None
self.installation = None self.installation = None
@@ -134,9 +134,9 @@ class ResultsPage(Adw.NavigationPage):
self.pending_page = None self.pending_page = None
self.loading = LoadingStatus(_("Searching"), _("This should only take a moment")) self.loading = LoadingStatus(_("Searching"), _("This should only take a moment"))
self.install_page = None self.install_page = None
# Connections # Connections
self.search_entry.connect("search-changed", self.on_search) self.search_entry.connect("search-changed", self.on_search)
# Apply # Apply
self.stack.add_child(self.loading) self.stack.add_child(self.loading)

View File

@@ -9,33 +9,33 @@ from .file_install_dialog import FileInstallDialog
class SelectPage(Adw.NavigationPage): class SelectPage(Adw.NavigationPage):
__gtype_name__ = "SelectPage" __gtype_name__ = "SelectPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
nav_view = gtc() nav_view = gtc()
results_page = gtc() results_page = gtc()
remotes_group = gtc() remotes_group = gtc()
add_remote_row = gtc() add_remote_row = gtc()
open_row = gtc() open_row = gtc()
def start_loading(self): def start_loading(self):
self.nav_view.pop() self.nav_view.pop()
for row in self.remote_rows: for row in self.remote_rows:
self.remotes_group.remove(row) self.remotes_group.remove(row)
self.remote_rows.clear() self.remote_rows.clear()
def end_loading(self): def end_loading(self):
for installation, remotes in HostInfo.remotes.items(): for installation, remotes in HostInfo.remotes.items():
for remote in remotes: for remote in remotes:
if remote.disabled: if remote.disabled:
continue continue
row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation), activatable=True) row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation), activatable=True)
row.add_suffix(Gtk.Image(icon_name="right-large-symbolic")) row.add_suffix(Gtk.Image(icon_name="right-large-symbolic"))
row.connect("activated", self.results_page.show_remote, remote, installation, self.nav_view) row.connect("activated", self.results_page.show_remote, remote, installation, self.nav_view)
self.remotes_group.add(row) self.remotes_group.add(row)
self.remote_rows.append(row) self.remote_rows.append(row)
self.remotes_group.set_visible(len(self.remote_rows) != 0) self.remotes_group.set_visible(len(self.remote_rows) != 0)
def local_install_apply_callback(self, installation, file_names): def local_install_apply_callback(self, installation, file_names):
install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row] install_page = HostInfo.main_window.pages[HostInfo.main_window.install_row]
requests = [] requests = []
@@ -47,25 +47,25 @@ class SelectPage(Adw.NavigationPage):
"package_names": [file.get_path()], "package_names": [file.get_path()],
"extra_flags": [], "extra_flags": [],
}) })
install_page.install_packages(requests) install_page.install_packages(requests)
def file_dialog_handler(self, files): def file_dialog_handler(self, files):
FileInstallDialog(self, files, self.local_install_apply_callback).present(HostInfo.main_window) FileInstallDialog(self, files, self.local_install_apply_callback).present(HostInfo.main_window)
def file_choose_callback(self, object, result): def file_choose_callback(self, object, result):
try: try:
files = object.open_multiple_finish(result) files = object.open_multiple_finish(result)
if not files: if not files:
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found to install"))) HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found to install")))
return return
self.file_dialog_handler(files) self.file_dialog_handler(files)
except GLib.GError as gle: except GLib.GError as gle:
if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2): if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2):
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast) HostInfo.main_window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast)
def on_open(self, *args): def on_open(self, *args):
file_filter = Gtk.FileFilter(name=_("Flatpaks")) file_filter = Gtk.FileFilter(name=_("Flatpaks"))
file_filter.add_suffix("flatpak") file_filter.add_suffix("flatpak")
@@ -76,13 +76,13 @@ class SelectPage(Adw.NavigationPage):
file_chooser.set_filters(filters) file_chooser.set_filters(filters)
file_chooser.set_default_filter(file_filter) file_chooser.set_default_filter(file_filter)
file_chooser.open_multiple(HostInfo.main_window, None, self.file_choose_callback) file_chooser.open_multiple(HostInfo.main_window, None, self.file_choose_callback)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.remote_rows = [] self.remote_rows = []
# Connections # Connections
self.add_remote_row.connect("activated", lambda *_: HostInfo.main_window.activate_row(HostInfo.main_window.remotes_row)) self.add_remote_row.connect("activated", lambda *_: HostInfo.main_window.activate_row(HostInfo.main_window.remotes_row))
self.nav_view.connect("popped", self.results_page.on_back) self.nav_view.connect("popped", self.results_page.on_back)

View File

@@ -32,10 +32,10 @@ from .error_toast import ErrorToast
class WarehouseApplication(Adw.Application): class WarehouseApplication(Adw.Application):
"""The main application singleton class.""" """The main application singleton class."""
troubleshooting = "OS: {os}\nWarehouse version: {wv}\nGTK: {gtk}\nlibadwaita: {adw}\nApp ID: {app_id}\nProfile: {profile}\nLanguage: {lang}" troubleshooting = "OS: {os}\nWarehouse version: {wv}\nGTK: {gtk}\nlibadwaita: {adw}\nApp ID: {app_id}\nProfile: {profile}\nLanguage: {lang}"
version = Config.VERSION version = Config.VERSION
def __init__(self): def __init__(self):
super().__init__( super().__init__(
application_id="io.github.flattool.Warehouse", application_id="io.github.flattool.Warehouse",
@@ -47,13 +47,13 @@ class WarehouseApplication(Adw.Application):
self.create_action("open-menu", lambda *_: self.props.active_window.main_menu.popup(), ["F10"]) self.create_action("open-menu", lambda *_: self.props.active_window.main_menu.popup(), ["F10"])
self.create_action("refresh", lambda *_: self.props.active_window.refresh_handler(), ["<primary>r", "F5"]) self.create_action("refresh", lambda *_: self.props.active_window.refresh_handler(), ["<primary>r", "F5"])
self.create_action("open-files", self.on_open_files_shortcut, ["<primary>o"]) self.create_action("open-files", self.on_open_files_shortcut, ["<primary>o"])
self.create_action("show-packages-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("p"), ["<primary>p"]) self.create_action("show-packages-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("p"), ["<primary>p"])
self.create_action("show-remotes-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("m"), ["<primary>m"]) self.create_action("show-remotes-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("m"), ["<primary>m"])
self.create_action("show-user-data-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("d"), ["<primary>d"]) self.create_action("show-user-data-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("d"), ["<primary>d"])
self.create_action("show-snapshots-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("s"), ["<primary>s"]) self.create_action("show-snapshots-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("s"), ["<primary>s"])
self.create_action("show-install-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("i"), ["<primary>i"]) self.create_action("show-install-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("i"), ["<primary>i"])
self.create_action("toggle-select-mode", self.on_toggle_select_mode_shortcut, ["<primary>b", "<primary>Return"]) self.create_action("toggle-select-mode", self.on_toggle_select_mode_shortcut, ["<primary>b", "<primary>Return"])
self.create_action("toggle-selection-kp-enter", self.on_toggle_select_mode_shortcut, ["<primary>KP_Enter"]) # Doesn't show in the shortcuts window self.create_action("toggle-selection-kp-enter", self.on_toggle_select_mode_shortcut, ["<primary>KP_Enter"]) # Doesn't show in the shortcuts window
self.create_action("search-mode", self.on_search_mode_shortcut, ["<primary>f"]) self.create_action("search-mode", self.on_search_mode_shortcut, ["<primary>f"])
@@ -61,9 +61,9 @@ class WarehouseApplication(Adw.Application):
self.create_action("new", self.on_new_shortcut, ["<primary>n"]) self.create_action("new", self.on_new_shortcut, ["<primary>n"])
self.create_action("active-data-view", lambda *_: self.on_data_view_shortcut(True), ["<Alt>1"]) self.create_action("active-data-view", lambda *_: self.on_data_view_shortcut(True), ["<Alt>1"])
self.create_action("leftover-data-view", lambda *_: self.on_data_view_shortcut(False), ["<Alt>2"]) self.create_action("leftover-data-view", lambda *_: self.on_data_view_shortcut(False), ["<Alt>2"])
self.is_dialog_open = False self.is_dialog_open = False
gtk_version = ( gtk_version = (
str(Gtk.MAJOR_VERSION) str(Gtk.MAJOR_VERSION)
+ "." + "."
@@ -80,7 +80,7 @@ class WarehouseApplication(Adw.Application):
) )
os_string = GLib.get_os_info("NAME") + " " + GLib.get_os_info("VERSION") os_string = GLib.get_os_info("NAME") + " " + GLib.get_os_info("VERSION")
lang = GLib.environ_getenv(GLib.get_environ(), "LANG") lang = GLib.environ_getenv(GLib.get_environ(), "LANG")
self.troubleshooting = self.troubleshooting.format( self.troubleshooting = self.troubleshooting.format(
os=os_string, os=os_string,
wv=self.version, wv=self.version,
@@ -90,22 +90,22 @@ class WarehouseApplication(Adw.Application):
app_id=self.get_application_id(), app_id=self.get_application_id(),
lang=lang, lang=lang,
) )
def on_open_files_shortcut(self, *args): def on_open_files_shortcut(self, *args):
window = self.props.active_window window = self.props.active_window
def file_choose_callback(object, result): def file_choose_callback(object, result):
try: try:
files = object.open_multiple_finish(result) files = object.open_multiple_finish(result)
if not files: if not files:
window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found")).toast) window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found")).toast)
return return
window.on_file_drop(None, files, None, None) window.on_file_drop(None, files, None, None)
except GLib.GError as gle: except GLib.GError as gle:
if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2): if not (gle.domain == "gtk-dialog-error-quark" and gle.code == 2):
window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast) window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), str(gle)).toast)
file_filter = Gtk.FileFilter(name=_("Flatpaks & Remotes")) file_filter = Gtk.FileFilter(name=_("Flatpaks & Remotes"))
file_filter.add_suffix("flatpak") file_filter.add_suffix("flatpak")
file_filter.add_suffix("flatpakref") file_filter.add_suffix("flatpakref")
@@ -116,63 +116,63 @@ class WarehouseApplication(Adw.Application):
file_chooser.set_filters(filters) file_chooser.set_filters(filters)
file_chooser.set_default_filter(file_filter) file_chooser.set_default_filter(file_filter)
file_chooser.open_multiple(window, None, file_choose_callback) file_chooser.open_multiple(window, None, file_choose_callback)
def on_toggle_select_mode_shortcut(self, *args): def on_toggle_select_mode_shortcut(self, *args):
try: try:
button = self.props.active_window.stack.get_visible_child().select_button button = self.props.active_window.stack.get_visible_child().select_button
button.set_active(not button.get_active()) button.set_active(not button.get_active())
except AttributeError: except AttributeError:
pass pass
def on_search_mode_shortcut(self, *args): def on_search_mode_shortcut(self, *args):
try: try:
button = self.props.active_window.stack.get_visible_child().search_button button = self.props.active_window.stack.get_visible_child().search_button
button.set_active(True) button.set_active(True)
except AttributeError: except AttributeError:
pass pass
def on_filter_shortcut(self, *args): def on_filter_shortcut(self, *args):
try: try:
button = self.props.active_window.stack.get_visible_child().filter_button button = self.props.active_window.stack.get_visible_child().filter_button
button.set_active(not button.get_active()) button.set_active(not button.get_active())
except AttributeError: except AttributeError:
pass pass
try: try:
button = self.props.active_window.stack.get_visible_child().sort_button button = self.props.active_window.stack.get_visible_child().sort_button
button.set_active(True) button.set_active(True)
except AttributeError: except AttributeError:
pass pass
try: try:
button = self.props.active_window.stack.get_visible_child().show_disabled_button button = self.props.active_window.stack.get_visible_child().show_disabled_button
if button.get_visible(): if button.get_visible():
button.set_active(not button.get_active()) button.set_active(not button.get_active())
except AttributeError: except AttributeError:
pass pass
def on_new_shortcut(self, *args): def on_new_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child() page = self.props.active_window.stack.get_visible_child()
try: try:
page.new_custom_handler() page.new_custom_handler()
except AttributeError: except AttributeError:
pass pass
try: try:
page.on_new() page.on_new()
except AttributeError: except AttributeError:
pass pass
def on_delete_shortcut(self, *args): def on_delete_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child() page = self.props.active_window.stack.get_visible_child()
try: try:
if not page.select_button.get_active(): if not page.select_button.get_active():
return return
page.select_trash_handler() page.select_trash_handler()
except AttributeError: except AttributeError:
pass pass
def on_data_view_shortcut(self, is_active): def on_data_view_shortcut(self, is_active):
page = self.props.active_window.stack.get_visible_child() page = self.props.active_window.stack.get_visible_child()
try: try:
@@ -181,10 +181,10 @@ class WarehouseApplication(Adw.Application):
page.stack.set_visible_child(adp if is_active else ldp) page.stack.set_visible_child(adp if is_active else ldp)
except AttributeError: except AttributeError:
pass pass
def do_activate(self): def do_activate(self):
"""Called when the application is activated. """Called when the application is activated.
We raise the application's main window, creating it if We raise the application's main window, creating it if
necessary. necessary.
""" """
@@ -192,7 +192,7 @@ class WarehouseApplication(Adw.Application):
if not win: if not win:
win = WarehouseWindow(application=self) win = WarehouseWindow(application=self)
win.present() win.present()
def on_about_action(self, widget, _a): def on_about_action(self, widget, _a):
"""Callback for the app.about action.""" """Callback for the app.about action."""
about = Adw.AboutDialog( about = Adw.AboutDialog(
@@ -233,14 +233,14 @@ class WarehouseApplication(Adw.Application):
], ],
) )
about.present(self.props.active_window) about.present(self.props.active_window)
def on_preferences_action(self, widget, _): def on_preferences_action(self, widget, _):
"""Callback for the app.preferences action.""" """Callback for the app.preferences action."""
print("app.preferences action activated") print("app.preferences action activated")
def create_action(self, name, callback, shortcuts=None): def create_action(self, name, callback, shortcuts=None):
"""Add an application action. """Add an application action.
Args: Args:
name: the name of the action name: the name of the action
callback: the function to be called when the action is activated callback: the function to be called when the action is activated
@@ -251,7 +251,7 @@ class WarehouseApplication(Adw.Application):
self.add_action(action) self.add_action(action)
if shortcuts: if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts) self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version): def main(version):
"""The application's entry point.""" """The application's entry point."""
app = WarehouseApplication() app = WarehouseApplication()

View File

@@ -80,7 +80,7 @@ template $WarehouseWindow: Adw.ApplicationWindow {
label: _("Remotes"); label: _("Remotes");
} }
} }
Box user_data_row { Box user_data_row {
margin-top: 12; margin-top: 12;
margin-bottom: 12; margin-bottom: 12;

View File

@@ -54,11 +54,11 @@ class WarehouseWindow(Adw.ApplicationWindow):
for _, page in self.pages.items(): for _, page in self.pages.items():
if page.instance: if page.instance:
page.instance.end_loading() page.instance.end_loading()
self.refresh_button.set_sensitive(True) self.refresh_button.set_sensitive(True)
self.refresh_requested = False self.refresh_requested = False
self.remove_refresh_lockout("refresh handler direct") self.remove_refresh_lockout("refresh handler direct")
def do_refresh(self): def do_refresh(self):
self.start_loading() self.start_loading()
self.refresh_button.set_sensitive(False) self.refresh_button.set_sensitive(False)
@@ -72,15 +72,15 @@ class WarehouseWindow(Adw.ApplicationWindow):
return return
else: else:
self.refresh_requested = True self.refresh_requested = True
def add_refresh_lockout(self, reason): def add_refresh_lockout(self, reason):
self.refresh_lockouts.append(reason) self.refresh_lockouts.append(reason)
self.refresh_button.set_sensitive(False) self.refresh_button.set_sensitive(False)
def remove_refresh_lockout(self, reason): def remove_refresh_lockout(self, reason):
if reason in self.refresh_lockouts: if reason in self.refresh_lockouts:
self.refresh_lockouts.remove(reason) self.refresh_lockouts.remove(reason)
if len(self.refresh_lockouts) == 0: if len(self.refresh_lockouts) == 0:
if self.refresh_requested: if self.refresh_requested:
self.do_refresh() self.do_refresh()
@@ -116,7 +116,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
if not page_found: if not page_found:
self.navigation_row_listbox.get_row_at_index(0).activate() self.navigation_row_listbox.get_row_at_index(0).activate()
def on_file_drop(self, drop_target, value, x, y): def on_file_drop(self, drop_target, value, x, y):
try: try:
paks = [] paks = []
@@ -135,7 +135,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
dialog.add_response("continue", _("OK")) dialog.add_response("continue", _("OK"))
dialog.present(self) dialog.present(self)
return return
if len(remotes) > 0 and len(paks) > 0: if len(remotes) > 0 and len(paks) > 0:
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Mixed Filetypes"), heading=_("Mixed Filetypes"),
@@ -145,7 +145,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
dialog.add_response("continue", _("OK")) dialog.add_response("continue", _("OK"))
dialog.present(self) dialog.present(self)
return return
if len(remotes) > 1: if len(remotes) > 1:
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Too Many Remotes"), heading=_("Too Many Remotes"),
@@ -154,7 +154,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
dialog.add_response("continue", _("OK")) dialog.add_response("continue", _("OK"))
dialog.present(self) dialog.present(self)
return return
if len(remotes) == 1: if len(remotes) == 1:
# Adding a remote # Adding a remote
self.activate_row(self.remotes_row) self.activate_row(self.remotes_row)
@@ -165,22 +165,22 @@ class WarehouseWindow(Adw.ApplicationWindow):
self.activate_row(self.install_row) self.activate_row(self.install_row)
install_page = self.pages[self.install_row] install_page = self.pages[self.install_row]
install_page.select_page.file_dialog_handler(paks) install_page.select_page.file_dialog_handler(paks)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open files"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open files"), str(e)).toast)
def on_drop_enter(self, *args): def on_drop_enter(self, *args):
self.main_split.add_css_class("blurred") self.main_split.add_css_class("blurred")
self.file_drop_revealer.set_reveal_child(True) self.file_drop_revealer.set_reveal_child(True)
return 1 return 1
def on_drop_leave(self, *args): def on_drop_leave(self, *args):
self.main_split.remove_css_class("blurred") self.main_split.remove_css_class("blurred")
self.file_drop_revealer.set_reveal_child(False) self.file_drop_revealer.set_reveal_child(False)
def switch_page_shortcut_handler(self, letter): def switch_page_shortcut_handler(self, letter):
self.activate_row(self.shortcut_to_pages[letter]) self.activate_row(self.shortcut_to_pages[letter])
def key_handler(self, controller, keyval, keycode, state): def key_handler(self, controller, keyval, keycode, state):
page = self.stack.get_visible_child() page = self.stack.get_visible_child()
if keyval == Gdk.KEY_BackSpace or keyval == Gdk.KEY_Delete: if keyval == Gdk.KEY_BackSpace or keyval == Gdk.KEY_Delete:
@@ -194,7 +194,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
page.on_escape_handler() page.on_escape_handler()
except AttributeError: except AttributeError:
pass pass
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@@ -239,7 +239,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
file_drop.connect("leave", self.on_drop_leave) file_drop.connect("leave", self.on_drop_leave)
event_controller.connect("key-pressed", self.key_handler) event_controller.connect("key-pressed", self.key_handler)
self.refresh_button.connect("clicked", self.refresh_handler) self.refresh_button.connect("clicked", self.refresh_handler)
# Apply again # Apply again
self.start_loading() self.start_loading()
HostInfo.get_flatpaks(callback=self.end_loading) HostInfo.get_flatpaks(callback=self.end_loading)

View File

@@ -16,7 +16,7 @@ class PackageInstallWorker:
}, },
] ]
""" """
groups = None groups = None
process = None process = None
callback = None callback = None
@@ -24,19 +24,19 @@ class PackageInstallWorker:
loading_status = None loading_status = None
total_groups = 0 total_groups = 0
cancelled = False cancelled = False
@classmethod @classmethod
def update_status(this, index, package_ratio, complete, total): def update_status(this, index, package_ratio, complete, total):
group_ratio = (package_ratio + complete) / (total or 1) group_ratio = (package_ratio + complete) / (total or 1)
final_ratio = (group_ratio + index) / (this.total_groups or 1) final_ratio = (group_ratio + index) / (this.total_groups or 1)
print(f"gr: {(package_ratio + complete) / (total or 1):.2f}, fr: {((package_ratio + complete) / (total or 1) + index) / (this.total_groups or 1):.2f}") print(f"gr: {(package_ratio + complete) / (total or 1):.2f}, fr: {((package_ratio + complete) / (total or 1) + index) / (this.total_groups or 1):.2f}")
print("i:", index, ", g:", this.total_groups, ", r:", package_ratio, ", c:", complete, ", t:", total) print("i:", index, ", g:", this.total_groups, ", r:", package_ratio, ", c:", complete, ", t:", total)
print("=======================================") print("=======================================")
if not this.loading_status is None: if not this.loading_status is None:
GLib.idle_add(lambda *_: this.loading_status.progress_bar.set_fraction(final_ratio)) GLib.idle_add(lambda *_: this.loading_status.progress_bar.set_fraction(final_ratio))
@classmethod @classmethod
def install_thread(this): def install_thread(this):
try: try:
@@ -44,20 +44,20 @@ class PackageInstallWorker:
for index, group in enumerate(this.groups): for index, group in enumerate(this.groups):
if this.cancelled: if this.cancelled:
return return
real_installation = "" real_installation = ""
installation = group['installation'] installation = group['installation']
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
real_installation = f"--{installation}" real_installation = f"--{installation}"
else: else:
real_installation = f"--installation={installation}" real_installation = f"--installation={installation}"
cmd = ['flatpak-spawn', '--host', 'flatpak', 'install', '-y'] cmd = ['flatpak-spawn', '--host', 'flatpak', 'install', '-y']
# Handle local file installs. They don't have a remote specified # Handle local file installs. They don't have a remote specified
if group['remote'] != "local_file": if group['remote'] != "local_file":
cmd.append(group['remote']) cmd.append(group['remote'])
cmd += [real_installation] + group['package_names'] + group['extra_flags'] cmd += [real_installation] + group['package_names'] + group['extra_flags']
this.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) this.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
percent_pattern = r'\d{1,3}%' percent_pattern = r'\d{1,3}%'
@@ -75,34 +75,34 @@ class PackageInstallWorker:
this.update_status(index, ratio, complete, total) this.update_status(index, ratio, complete, total)
else: else:
this.update_status(index, ratio, 0, 1) this.update_status(index, ratio, 0, 1)
this.process.wait(timeout=10) this.process.wait(timeout=10)
if error := this.process.communicate()[1].strip(): if error := this.process.communicate()[1].strip():
errors.append(error) errors.append(error)
if len(errors) > 0: if len(errors) > 0:
this.on_error(_("Errors occurred during installation"), "\n".join(errors)) this.on_error(_("Errors occurred during installation"), "\n".join(errors))
except subprocess.TimeoutExpired as te: except subprocess.TimeoutExpired as te:
this.process.terminate() this.process.terminate()
this.on_error(_("Error occurred during installation"), _("Failed to exit cleanly")) this.on_error(_("Error occurred during installation"), _("Failed to exit cleanly"))
except Exception as e: except Exception as e:
this.process.terminate() this.process.terminate()
this.on_error(_("Error occurred during installation"), str(e)) this.on_error(_("Error occurred during installation"), str(e))
@classmethod @classmethod
def cancel(this): def cancel(this):
if this.process is None: if this.process is None:
return return
try: try:
this.cancelled = True this.cancelled = True
this.process.terminate() this.process.terminate()
this.process.wait(timeout=10) this.process.wait(timeout=10)
except Exception as e: except Exception as e:
this.on_error(_("Could not cancel installation"), str(e)) this.on_error(_("Could not cancel installation"), str(e))
@classmethod @classmethod
def on_done(this, *args): def on_done(this, *args):
this.process = None this.process = None
@@ -110,31 +110,31 @@ class PackageInstallWorker:
HostInfo.main_window.remove_refresh_lockout("installing packages") HostInfo.main_window.remove_refresh_lockout("installing packages")
if not this.loading_status is None: if not this.loading_status is None:
this.loading_status.progress_bar.set_fraction(0.0) this.loading_status.progress_bar.set_fraction(0.0)
if not this.callback is None: if not this.callback is None:
this.callback() this.callback()
@classmethod @classmethod
def on_error(this, user_facing_label, error_message): def on_error(this, user_facing_label, error_message):
if not this.error_callback is None: if not this.error_callback is None:
this.error_callback(user_facing_label, error_message) this.error_callback(user_facing_label, error_message)
@classmethod @classmethod
def install(this, groups, loading_status=None, callback=None, error_callback=None): def install(this, groups, loading_status=None, callback=None, error_callback=None):
if not this.process is None: if not this.process is None:
this.on_error(_("Could not install packages"), _("Packages are currently being installed.")) this.on_error(_("Could not install packages"), _("Packages are currently being installed."))
return False return False
this.callback = callback this.callback = callback
this.groups = groups this.groups = groups
this.total_groups = len(groups) this.total_groups = len(groups)
this.loading_status = loading_status this.loading_status = loading_status
this.error_callback = error_callback this.error_callback = error_callback
if this.total_groups < 1: if this.total_groups < 1:
this.on_error(_("Could not install packages"), _("No packages were requested to be installed.")) this.on_error(_("Could not install packages"), _("No packages were requested to be installed."))
return False return False
HostInfo.main_window.add_refresh_lockout("installing packages") HostInfo.main_window.add_refresh_lockout("installing packages")
Gio.Task.new(None, None, this.on_done).run_in_thread(lambda *_: this.install_thread()) Gio.Task.new(None, None, this.on_done).run_in_thread(lambda *_: this.install_thread())
return True return True

View File

@@ -60,7 +60,7 @@ class FiltersPage(Adw.NavigationPage):
def app_check_handler(self, *args): def app_check_handler(self, *args):
self.show_apps = self.app_check.get_active() self.show_apps = self.app_check.get_active()
self.update_gsettings() self.update_gsettings()
def runtime_check_handler(self, *args): def runtime_check_handler(self, *args):
self.show_runtimes = self.runtime_check.get_active() self.show_runtimes = self.runtime_check.get_active()
self.update_gsettings() self.update_gsettings()
@@ -76,21 +76,21 @@ class FiltersPage(Adw.NavigationPage):
self.remotes_string += f"{row.item.name}<>{row.installation};" self.remotes_string += f"{row.item.name}<>{row.installation};"
elif state: elif state:
self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "") self.remotes_string.replace(f"{row.item.name}<>{row.installation};", "")
self.update_gsettings() self.update_gsettings()
def all_runtimes_handler(self, switch, state): def all_runtimes_handler(self, switch, state):
self.runtimes_string = "" self.runtimes_string = ""
if not state: if not state:
self.runtimes_string = "all" self.runtimes_string = "all"
for row in self.runtime_rows: for row in self.runtime_rows:
row.set_visible(state) row.set_visible(state)
if state and row.check_button.get_active(): if state and row.check_button.get_active():
self.runtimes_string += f"{row.item};" self.runtimes_string += f"{row.item};"
elif state: elif state:
self.runtimes_string.replace(f"{row.item};", "") self.runtimes_string.replace(f"{row.item};", "")
self.update_gsettings() self.update_gsettings()
def remote_row_check_handler(self, row): def remote_row_check_handler(self, row):
@@ -160,7 +160,7 @@ class FiltersPage(Adw.NavigationPage):
self.show_runtimes = self.settings.get_boolean("show-runtimes") self.show_runtimes = self.settings.get_boolean("show-runtimes")
self.remotes_string = self.settings.get_string("remotes-list") self.remotes_string = self.settings.get_string("remotes-list")
self.runtimes_string = self.settings.get_string("runtimes-list") self.runtimes_string = self.settings.get_string("runtimes-list")
self.app_check.set_active(self.show_apps) self.app_check.set_active(self.show_apps)
self.runtime_check.set_active(self.show_runtimes) self.runtime_check.set_active(self.show_runtimes)

View File

@@ -47,42 +47,41 @@ class PackagesPage(Adw.BreakpointBin):
uninstall_button = gtc() uninstall_button = gtc()
properties_page = gtc() properties_page = gtc()
filters_page = gtc() filters_page = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
instance = None instance = None
page_name = "packages" page_name = "packages"
last_activated_row = None last_activated_row = None
def set_status(self, to_set): def set_status(self, to_set):
if to_set is self.scrolled_window: if to_set is self.scrolled_window:
self.properties_page.stack.set_visible_child(self.properties_page.nav_view) self.properties_page.stack.set_visible_child(self.properties_page.nav_view)
self.select_button.set_sensitive(True) self.select_button.set_sensitive(True)
self.filter_button.set_sensitive(True) self.filter_button.set_sensitive(True)
self.filters_page.set_sensitive(True) self.filters_page.set_sensitive(True)
self.search_button.set_sensitive(True) self.search_button.set_sensitive(True)
self.search_entry.set_editable(True) self.search_entry.set_editable(True)
else: else:
self.select_button.set_sensitive(False) self.select_button.set_sensitive(False)
if to_set is self.no_packages: if to_set is self.no_packages:
self.properties_page.stack.set_visible_child(self.properties_page.error_tbv) self.properties_page.stack.set_visible_child(self.properties_page.error_tbv)
self.filter_button.set_sensitive(False) self.filter_button.set_sensitive(False)
self.filter_button.set_active(False) self.filter_button.set_active(False)
if to_set is self.no_filter_results: if to_set is self.no_filter_results:
self.properties_page.stack.set_visible_child(self.properties_page.error_tbv) self.properties_page.stack.set_visible_child(self.properties_page.error_tbv)
self.filter_button.set_sensitive(True) self.filter_button.set_sensitive(True)
self.filters_page.set_sensitive(True) self.filters_page.set_sensitive(True)
if not self.packages_split.get_collapsed(): if not self.packages_split.get_collapsed():
self.filter_button.set_active(True) self.filter_button.set_active(True)
if to_set is self.no_results: if to_set is self.no_results:
self.filters_page.set_sensitive(False) self.filters_page.set_sensitive(False)
if to_set is self.loading_packages: if to_set is self.loading_packages:
self.stack.set_visible_child(self.loading_view) self.stack.set_visible_child(self.loading_view)
elif to_set is self.uninstalling: elif to_set is self.uninstalling:
@@ -94,7 +93,7 @@ class PackagesPage(Adw.BreakpointBin):
else: else:
self.stack.set_visible_child(self.packages_split) self.stack.set_visible_child(self.packages_split)
self.status_stack.set_visible_child(to_set) self.status_stack.set_visible_child(to_set)
def apply_filters(self): def apply_filters(self):
i = 0 i = 0
show_apps = self.filter_settings.get_boolean("show-apps") show_apps = self.filter_settings.get_boolean("show-apps")
@@ -113,20 +112,20 @@ class PackagesPage(Adw.BreakpointBin):
visible = False visible = False
if runtimes_list != "all" and (row.package.is_runtime or row.package.dependent_runtime and not row.package.dependent_runtime.info["ref"] in runtimes_list): if runtimes_list != "all" and (row.package.is_runtime or row.package.dependent_runtime and not row.package.dependent_runtime.info["ref"] in runtimes_list):
visible = False visible = False
row.set_visible(visible) row.set_visible(visible)
if visible: if visible:
total_visible += 1 total_visible += 1
else: else:
row.check_button.set_active(False) row.check_button.set_active(False)
if total_visible == 0: if total_visible == 0:
self.set_status(self.no_filter_results) self.set_status(self.no_filter_results)
else: else:
GLib.idle_add(lambda *_: self.set_status(self.scrolled_window)) GLib.idle_add(lambda *_: self.set_status(self.scrolled_window))
if self.current_row_for_properties and not self.current_row_for_properties.get_visible(): if self.current_row_for_properties and not self.current_row_for_properties.get_visible():
self.select_first_visible_row() self.select_first_visible_row()
def select_first_visible_row(self): def select_first_visible_row(self):
first_visible_row = None first_visible_row = None
i = 0 i = 0
@@ -136,17 +135,17 @@ class PackagesPage(Adw.BreakpointBin):
first_visible_row = row first_visible_row = row
self.current_row_for_properties = row self.current_row_for_properties = row
break break
if not first_visible_row is None: if not first_visible_row is None:
self.packages_list_box.select_row(first_visible_row) self.packages_list_box.select_row(first_visible_row)
self.properties_page.set_properties(first_visible_row.package) self.properties_page.set_properties(first_visible_row.package)
def row_select_handler(self, row): def row_select_handler(self, row):
if row.check_button.get_active(): if row.check_button.get_active():
self.selected_rows.append(row) self.selected_rows.append(row)
else: else:
self.selected_rows.remove(row) self.selected_rows.remove(row)
if (total := len(self.selected_rows)) > 0: if (total := len(self.selected_rows)) > 0:
self.packages_navpage.set_title(_("{} Selected").format(total)) self.packages_navpage.set_title(_("{} Selected").format(total))
self.copy_button.set_sensitive(True) self.copy_button.set_sensitive(True)
@@ -155,17 +154,17 @@ class PackagesPage(Adw.BreakpointBin):
self.packages_navpage.set_title(_("Packages")) self.packages_navpage.set_title(_("Packages"))
self.copy_button.set_sensitive(False) self.copy_button.set_sensitive(False)
self.uninstall_button.set_sensitive(False) self.uninstall_button.set_sensitive(False)
def select_all_handler(self, *args): def select_all_handler(self, *args):
i = 0 i = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_active(row.get_visible()) row.check_button.set_active(row.get_visible())
def row_rclick_handler(self, row): def row_rclick_handler(self, row):
self.select_button.set_active(True) self.select_button.set_active(True)
GLib.idle_add(lambda *_, button=row.check_button: button.set_active(not button.get_active())) GLib.idle_add(lambda *_, button=row.check_button: button.set_active(not button.get_active()))
def generate_list(self, *args): def generate_list(self, *args):
self.properties_page.nav_view.pop_to_page(self.properties_page.inner_nav_page) self.properties_page.nav_view.pop_to_page(self.properties_page.inner_nav_page)
self.packages_list_box.remove_all() self.packages_list_box.remove_all()
@@ -176,7 +175,7 @@ class PackagesPage(Adw.BreakpointBin):
if len(HostInfo.flatpaks) == 0: if len(HostInfo.flatpaks) == 0:
self.set_status(self.no_packages) self.set_status(self.no_packages)
return return
for package in HostInfo.flatpaks: for package in HostInfo.flatpaks:
row = AppRow(package, self.row_rclick_handler) row = AppRow(package, self.row_rclick_handler)
package.app_row = row package.app_row = row
@@ -190,26 +189,26 @@ class PackagesPage(Adw.BreakpointBin):
row.eol_runtime_status_icon.set_visible(package.dependent_runtime.is_eol) row.eol_runtime_status_icon.set_visible(package.dependent_runtime.is_eol)
except Exception as e: except Exception as e:
self.packages_toast_overlay.add_toast(ErrorToast(_("Error getting Flatpak '{}'").format(package.info["name"]), str(e)).toast) self.packages_toast_overlay.add_toast(ErrorToast(_("Error getting Flatpak '{}'").format(package.info["name"]), str(e)).toast)
self.packages_list_box.append(row) self.packages_list_box.append(row)
self.apply_filters() self.apply_filters()
self.select_first_visible_row() self.select_first_visible_row()
self.scrolled_window.set_vadjustment(Gtk.Adjustment.new(0,0,0,0,0,0)) # Scroll list to top self.scrolled_window.set_vadjustment(Gtk.Adjustment.new(0,0,0,0,0,0)) # Scroll list to top
def row_activate_handler(self, list_box, row): def row_activate_handler(self, list_box, row):
if self.select_button.get_active(): if self.select_button.get_active():
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
return return
self.last_activated_row = row self.last_activated_row = row
self.properties_page.set_properties(row.package) self.properties_page.set_properties(row.package)
self.properties_page.nav_view.pop() self.properties_page.nav_view.pop()
self.packages_split.set_show_content(True) self.packages_split.set_show_content(True)
self.filter_button.set_active(False) self.filter_button.set_active(False)
self.current_row_for_properties = row self.current_row_for_properties = row
def filter_func(self, row): def filter_func(self, row):
search_text = self.search_entry.get_text().lower() search_text = self.search_entry.get_text().lower()
title = row.get_title().lower() title = row.get_title().lower()
@@ -217,20 +216,20 @@ class PackagesPage(Adw.BreakpointBin):
if row.get_visible() and (search_text in title or search_text in subtitle): if row.get_visible() and (search_text in title or search_text in subtitle):
self.is_result = True self.is_result = True
return True return True
def set_selection_mode(self, is_enabled): def set_selection_mode(self, is_enabled):
if is_enabled: if is_enabled:
self.packages_list_box.set_selection_mode(Gtk.SelectionMode.NONE) self.packages_list_box.set_selection_mode(Gtk.SelectionMode.NONE)
else: else:
self.packages_list_box.set_selection_mode(Gtk.SelectionMode.SINGLE) self.packages_list_box.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.packages_list_box.select_row(self.last_activated_row) self.packages_list_box.select_row(self.last_activated_row)
i = 0 i = 0
while row := self.packages_list_box.get_row_at_index(i): while row := self.packages_list_box.get_row_at_index(i):
i += 1 i += 1
GLib.idle_add(row.check_button.set_active, False) GLib.idle_add(row.check_button.set_active, False)
GLib.idle_add(row.check_button.set_visible, is_enabled) GLib.idle_add(row.check_button.set_visible, is_enabled)
def selection_copy(self, box, row): def selection_copy(self, box, row):
self.copy_pop.popdown() self.copy_pop.popdown()
info = "" info = ""
@@ -245,7 +244,7 @@ class PackagesPage(Adw.BreakpointBin):
case self.copy_refs: case self.copy_refs:
info = "ref" info = "ref"
feedback = _("Refs") feedback = _("Refs")
to_copy = [] to_copy = []
for row in self.selected_rows: for row in self.selected_rows:
to_copy.append(row.package.info[info]) to_copy.append(row.package.info[info])
@@ -255,11 +254,11 @@ class PackagesPage(Adw.BreakpointBin):
self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(feedback))) self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(feedback)))
except Exception as e: except Exception as e:
self.packages_toast_overlay.add_toast(ErrorToast(_("Could not copy {}").format(feedback), str(e)).toast) self.packages_toast_overlay.add_toast(ErrorToast(_("Could not copy {}").format(feedback), str(e)).toast)
def selection_uninstall(self, *args): def selection_uninstall(self, *args):
if len(self.selected_rows) < 1 or not self.uninstall_button.get_sensitive(): if len(self.selected_rows) < 1 or not self.uninstall_button.get_sensitive():
return return
def on_response(should_trash): def on_response(should_trash):
GLib.idle_add(lambda *_: self.set_status(self.uninstalling)) GLib.idle_add(lambda *_: self.set_status(self.uninstalling))
error = [] error = []
@@ -268,37 +267,37 @@ class PackagesPage(Adw.BreakpointBin):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y'] cmd = ['flatpak-spawn', '--host', 'flatpak', 'uninstall', '-y']
to_uninstall = {} # { <remote><><installation>: [<ref1>, <ref2>, <ref3>, ...], ... } to_uninstall = {} # { <remote><><installation>: [<ref1>, <ref2>, <ref3>, ...], ... }
to_trash = [] to_trash = []
for row in self.selected_rows: for row in self.selected_rows:
key = row.package.info['installation'] key = row.package.info['installation']
if ls := to_uninstall.get(key, False): if ls := to_uninstall.get(key, False):
ls.append(row.package.info['ref']) ls.append(row.package.info['ref'])
else: else:
to_uninstall[key] = [row.package.info['ref']] to_uninstall[key] = [row.package.info['ref']]
if should_trash and os.path.exists(row.package.data_path): if should_trash and os.path.exists(row.package.data_path):
to_trash.append(row.package.data_path) to_trash.append(row.package.data_path)
for installation, packages in to_uninstall.items(): for installation, packages in to_uninstall.items():
suffix = [] suffix = []
if installation == "user" or installation == "system": if installation == "user" or installation == "system":
suffix.append(f"--{installation}") suffix.append(f"--{installation}")
else: else:
suffix.append(f"--installation={installation}") suffix.append(f"--installation={installation}")
try: try:
subprocess.run(cmd + suffix + packages, check=True, text=True, capture_output=True) subprocess.run(cmd + suffix + packages, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error.append(str(cpe.stderr)) error.append(str(cpe.stderr))
except Exception as e: except Exception as e:
error.append(str(e)) error.append(str(e))
if should_trash and len(to_trash) > 0: if should_trash and len(to_trash) > 0:
try: try:
subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True) subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error.append(cpe) error.append(cpe)
def callback(*args): def callback(*args):
self.main_window.refresh_handler() self.main_window.refresh_handler()
HostInfo.main_window.remove_refresh_lockout("batch uninstalling packages") HostInfo.main_window.remove_refresh_lockout("batch uninstalling packages")
@@ -307,25 +306,25 @@ class PackagesPage(Adw.BreakpointBin):
GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(ErrorToast(_("Errors occurred while uninstalling"), details).toast)) GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(ErrorToast(_("Errors occurred while uninstalling"), details).toast))
else: else:
GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled Packages")))) GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled Packages"))))
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = UninstallDialog(on_response, True) dialog = UninstallDialog(on_response, True)
dialog.present(self.main_window) dialog.present(self.main_window)
def start_loading(self): def start_loading(self):
self.search_button.set_active(False) self.search_button.set_active(False)
self.last_activated_row = None self.last_activated_row = None
self.packages_navpage.set_title(_("Packages")) self.packages_navpage.set_title(_("Packages"))
self.select_button.set_active(False) self.select_button.set_active(False)
self.set_status(self.loading_packages) self.set_status(self.loading_packages)
def end_loading(self): def end_loading(self):
GLib.idle_add(lambda *_: self.generate_list()) GLib.idle_add(lambda *_: self.generate_list())
def select_button_handler(self, button): def select_button_handler(self, button):
self.set_selection_mode(button.get_active()) self.set_selection_mode(button.get_active())
def filter_button_handler(self, button): def filter_button_handler(self, button):
if button.get_active(): if button.get_active():
self.content_stack.set_visible_child(self.filters_page) self.content_stack.set_visible_child(self.filters_page)
@@ -333,35 +332,35 @@ class PackagesPage(Adw.BreakpointBin):
else: else:
self.content_stack.set_visible_child(self.properties_page) self.content_stack.set_visible_child(self.properties_page)
self.packages_split.set_show_content(False) self.packages_split.set_show_content(False)
def filter_page_handler(self, *args): def filter_page_handler(self, *args):
if self.packages_split.get_collapsed() and not self.packages_split.get_show_content(): if self.packages_split.get_collapsed() and not self.packages_split.get_show_content():
self.filter_button.set_active(False) self.filter_button.set_active(False)
def on_invalidate(self, row): def on_invalidate(self, row):
current_status = self.status_stack.get_visible_child() current_status = self.status_stack.get_visible_child()
if not current_status is self.no_results: if not current_status is self.no_results:
self.prev_status = current_status self.prev_status = current_status
self.is_result = False self.is_result = False
self.packages_list_box.invalidate_filter() self.packages_list_box.invalidate_filter()
if self.is_result: if self.is_result:
self.set_status(self.prev_status) self.set_status(self.prev_status)
else: else:
self.set_status(self.no_results) self.set_status(self.no_results)
def sort_func(self, row1, row2): def sort_func(self, row1, row2):
return row1.package.info["name"].lower() > row2.package.info["name"].lower() return row1.package.info["name"].lower() > row2.package.info["name"].lower()
def on_escape_handler(self): def on_escape_handler(self):
if self.select_button.get_active(): if self.select_button.get_active():
self.select_button.set_active(False) self.select_button.set_active(False)
elif self.filter_button.get_active(): elif self.filter_button.get_active():
self.filter_button.set_active(False) self.filter_button.set_active(False)
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.main_window = main_window self.main_window = main_window
self.loading_packages = LoadingStatus(_("Loading Packages"), _("This should only take a moment")) self.loading_packages = LoadingStatus(_("Loading Packages"), _("This should only take a moment"))
@@ -377,7 +376,7 @@ class PackagesPage(Adw.BreakpointBin):
self.selected_rows = [] self.selected_rows = []
self.current_row_for_properties = None self.current_row_for_properties = None
self.on_backspace_handler = self.selection_uninstall self.on_backspace_handler = self.selection_uninstall
# Apply # Apply
self.loading_view.set_content(self.loading_packages) self.loading_view.set_content(self.loading_packages)
self.packages_list_box.set_filter_func(self.filter_func) self.packages_list_box.set_filter_func(self.filter_func)
@@ -385,7 +384,7 @@ class PackagesPage(Adw.BreakpointBin):
self.properties_page.packages_page = self self.properties_page.packages_page = self
self.filters_page.packages_page = self self.filters_page.packages_page = self
self.__class__.instance = self self.__class__.instance = self
# Connections # Connections
self.search_entry.connect("search-changed", self.on_invalidate) self.search_entry.connect("search-changed", self.on_invalidate)
self.search_bar.set_key_capture_widget(main_window) self.search_bar.set_key_capture_widget(main_window)

View File

@@ -4,35 +4,35 @@ from gi.repository import Adw, Gtk, GLib
class UninstallDialog(Adw.AlertDialog): class UninstallDialog(Adw.AlertDialog):
__gtype_name__ = "UninstallDialog" __gtype_name__ = "UninstallDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
group = gtc() group = gtc()
trash = gtc() trash = gtc()
is_open = False is_open = False
def on_response(self, dialog, response): def on_response(self, dialog, response):
self.__class__.is_open = False self.__class__.is_open = False
if response != "continue": if response != "continue":
return return
self.continue_callback(self.trash.get_active()) self.continue_callback(self.trash.get_active())
def present(self, *args, **kwargs): def present(self, *args, **kwargs):
if self.__class__.is_open: if self.__class__.is_open:
return return
self.__class__.is_open = True self.__class__.is_open = True
super().present(*args, **kwargs) super().present(*args, **kwargs)
def __init__(self, continue_callback, show_trash_option, package_name=None, **kwargs): def __init__(self, continue_callback, show_trash_option, package_name=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
if package_name: if package_name:
self.set_heading(GLib.markup_escape_text(_("Uninstall {}?").format(package_name))) self.set_heading(GLib.markup_escape_text(_("Uninstall {}?").format(package_name)))
self.set_body(GLib.markup_escape_text(_("It will not be possible to use {} after removal").format(package_name))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use {} after removal").format(package_name)))
else: else:
self.set_heading(GLib.markup_escape_text(_("Uninstall Packages?"))) self.set_heading(GLib.markup_escape_text(_("Uninstall Packages?")))
self.set_body(GLib.markup_escape_text(_("It will not be possible to use these packages after removal"))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use these packages after removal")))
self.continue_callback = continue_callback self.continue_callback = continue_callback
self.add_response("cancel", _("Cancel")) self.add_response("cancel", _("Cancel"))
self.add_response("continue", _("Uninstall")) self.add_response("continue", _("Uninstall"))

View File

@@ -43,7 +43,7 @@ template $PropertiesPage : Adw.NavigationPage {
margin-bottom: 12; margin-bottom: 12;
orientation: vertical; orientation: vertical;
halign: fill; halign: fill;
Image app_icon { Image app_icon {
pixel-size: 100; pixel-size: 100;
margin-top: 6; margin-top: 6;

View File

@@ -14,10 +14,10 @@ class PropertiesPage(Adw.NavigationPage):
stack = gtc() stack = gtc()
error_tbv = gtc() error_tbv = gtc()
loading_tbv = gtc() loading_tbv = gtc()
more_menu = gtc() more_menu = gtc()
more_list = gtc() more_list = gtc()
nav_view = gtc() nav_view = gtc()
inner_nav_page = gtc() inner_nav_page = gtc()
toast_overlay = gtc() toast_overlay = gtc()
@@ -29,7 +29,7 @@ class PropertiesPage(Adw.NavigationPage):
eol_box = gtc() eol_box = gtc()
open_app_button = gtc() open_app_button = gtc()
uninstall_button = gtc() uninstall_button = gtc()
pin_row = gtc() pin_row = gtc()
pin_switch = gtc() pin_switch = gtc()
data_row = gtc() data_row = gtc()
@@ -44,30 +44,30 @@ class PropertiesPage(Adw.NavigationPage):
installed_size_row = gtc() installed_size_row = gtc()
runtime_row = gtc() runtime_row = gtc()
eol_package_package_status_icon = gtc() eol_package_package_status_icon = gtc()
id_row = gtc() id_row = gtc()
ref_row = gtc() ref_row = gtc()
arch_row = gtc() arch_row = gtc()
branch_row = gtc() branch_row = gtc()
license_row = gtc() license_row = gtc()
sdk_row = gtc() sdk_row = gtc()
origin_row = gtc() origin_row = gtc()
collection_row = gtc() collection_row = gtc()
installation_row = gtc() installation_row = gtc()
commit_row = gtc() commit_row = gtc()
parent_row = gtc() parent_row = gtc()
subject_row = gtc() subject_row = gtc()
date_row = gtc() date_row = gtc()
package = None package = None
def set_properties(self, package, refresh=False): def set_properties(self, package, refresh=False):
if package == self.package and not refresh: if package == self.package and not refresh:
# Do not update the ui if the same app row is clicked # Do not update the ui if the same app row is clicked
return return
self.reinstall_did_error = False self.reinstall_did_error = False
self.package = package self.package = package
pkg_name = package.info["name"] pkg_name = package.info["name"]
@@ -78,12 +78,12 @@ class PropertiesPage(Adw.NavigationPage):
else: else:
self.name.set_visible(False) self.name.set_visible(False)
self.inner_nav_page.set_title(_("Properties")) self.inner_nav_page.set_title(_("Properties"))
if package.icon_path: if package.icon_path:
GLib.idle_add(lambda *_: self.app_icon.set_from_file(package.icon_path)) GLib.idle_add(lambda *_: self.app_icon.set_from_file(package.icon_path))
else: else:
GLib.idle_add(lambda *_: self.app_icon.set_from_icon_name("application-x-executable-symbolic")) GLib.idle_add(lambda *_: self.app_icon.set_from_icon_name("application-x-executable-symbolic"))
self.eol_box.set_visible(package.is_eol) self.eol_box.set_visible(package.is_eol)
self.pin_row.set_visible(package.is_runtime) self.pin_row.set_visible(package.is_runtime)
self.open_app_button.set_visible(package.is_runtime) self.open_app_button.set_visible(package.is_runtime)
@@ -96,29 +96,29 @@ class PropertiesPage(Adw.NavigationPage):
has_path = os.path.exists(package.data_path) has_path = os.path.exists(package.data_path)
self.trash_data_button.set_sensitive(has_path and self.package.info['id'] != "io.github.flattool.Warehouse") self.trash_data_button.set_sensitive(has_path and self.package.info['id'] != "io.github.flattool.Warehouse")
self.open_data_button.set_sensitive(has_path) self.open_data_button.set_sensitive(has_path)
if not self.package.dependent_runtime is None: if not self.package.dependent_runtime is None:
self.runtime_row.set_visible(True) self.runtime_row.set_visible(True)
self.runtime_row.set_subtitle(self.package.dependent_runtime.info["name"]) self.runtime_row.set_subtitle(self.package.dependent_runtime.info["name"])
self.eol_package_package_status_icon.set_visible(self.package.dependent_runtime.is_eol) self.eol_package_package_status_icon.set_visible(self.package.dependent_runtime.is_eol)
if has_path: if has_path:
self.trash_data_button.set_visible(False) self.trash_data_button.set_visible(False)
self.open_data_button.set_visible(False) self.open_data_button.set_visible(False)
self.data_spinner.set_visible(True) self.data_spinner.set_visible(True)
self.data_row.set_subtitle(_("Loading User Data")) self.data_row.set_subtitle(_("Loading User Data"))
def callback(size): def callback(size):
self.trash_data_button.set_visible(True) self.trash_data_button.set_visible(True)
self.open_data_button.set_visible(True) self.open_data_button.set_visible(True)
self.data_spinner.set_visible(False) self.data_spinner.set_visible(False)
self.data_row.set_subtitle(size) self.data_row.set_subtitle(size)
self.package.get_data_size(lambda size: callback(size)) self.package.get_data_size(lambda size: callback(size))
else: else:
self.data_row.set_subtitle(_("No User Data")) self.data_row.set_subtitle(_("No User Data"))
self.data_spinner.set_visible(False) self.data_spinner.set_visible(False)
cli_info = None cli_info = None
try: try:
cli_info = package.get_cli_info() cli_info = package.get_cli_info()
@@ -128,10 +128,10 @@ class PropertiesPage(Adw.NavigationPage):
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast)
return return
for key, row in self.info_rows.items(): for key, row in self.info_rows.items():
row.set_visible(False) row.set_visible(False)
try: try:
subtitle = cli_info[key] subtitle = cli_info[key]
row.set_subtitle(subtitle) row.set_subtitle(subtitle)
@@ -144,7 +144,7 @@ class PropertiesPage(Adw.NavigationPage):
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not get properties"), str(e)).toast)
continue continue
self.mask_label.set_visible(package.is_masked) self.mask_label.set_visible(package.is_masked)
self.mask_switch.set_active(package.is_masked) self.mask_switch.set_active(package.is_masked)
self.pin_switch.set_active(package.is_pinned) self.pin_switch.set_active(package.is_pinned)
@@ -153,14 +153,14 @@ class PropertiesPage(Adw.NavigationPage):
if self.open_app_button.get_visible(): if self.open_app_button.get_visible():
self.more_list.append(self.view_snapshots) self.more_list.append(self.view_snapshots)
self.more_list.append(self.copy_launch_command) self.more_list.append(self.copy_launch_command)
self.more_list.append(self.show_details) self.more_list.append(self.show_details)
self.more_list.append(self.reinstall) self.more_list.append(self.reinstall)
def open_data_handler(self, *args): def open_data_handler(self, *args):
if error := self.package.open_data(): if error := self.package.open_data():
self.toast_overlay.add_toast(ErrorToast(_("Could not open data"), str(error)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open data"), str(error)).toast)
def trash_data_handler(self, *args): def trash_data_handler(self, *args):
def on_choice(dialog, response): def on_choice(dialog, response):
if response != 'continue': if response != 'continue':
@@ -176,12 +176,12 @@ class PropertiesPage(Adw.NavigationPage):
snapshot_list_package = snapshot_list_page.package_or_folder snapshot_list_package = snapshot_list_page.package_or_folder
if not snapshot_list_package is None: if not snapshot_list_package is None:
snapshot_list_page.set_snapshots(snapshot_list_package, True) snapshot_list_page.set_snapshots(snapshot_list_package, True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Send {}'s User Data to the Trash?").format(self.package.info["name"]), heading=_("Send {}'s User Data to the Trash?").format(self.package.info["name"]),
body=_("Your settings and data for this app will be sent to the trash") body=_("Your settings and data for this app will be sent to the trash")
@@ -191,7 +191,7 @@ class PropertiesPage(Adw.NavigationPage):
dialog.connect("response", on_choice) dialog.connect("response", on_choice)
dialog.set_response_appearance('continue', Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance('continue', Adw.ResponseAppearance.DESTRUCTIVE)
dialog.present(self.main_window) dialog.present(self.main_window)
def set_mask_handler(self, *args): def set_mask_handler(self, *args):
state = not self.mask_switch.get_active() state = not self.mask_switch.get_active()
def callback(*args): def callback(*args):
@@ -207,9 +207,9 @@ class PropertiesPage(Adw.NavigationPage):
GLib.idle_add(lambda *_: self.mask_switch.set_active(state)) GLib.idle_add(lambda *_: self.mask_switch.set_active(state))
GLib.idle_add(lambda *_: self.mask_label.set_visible(state)) GLib.idle_add(lambda *_: self.mask_label.set_visible(state))
self.package.app_row.masked_status_icon.set_visible(state) self.package.app_row.masked_status_icon.set_visible(state)
self.package.set_mask(state, callback) self.package.set_mask(state, callback)
def set_pin_handler(self, *args): def set_pin_handler(self, *args):
state = not self.pin_switch.get_active() state = not self.pin_switch.get_active()
def callback(*args): def callback(*args):
@@ -223,9 +223,9 @@ class PropertiesPage(Adw.NavigationPage):
self.toast_overlay.add_toast(Adw.Toast(title=response)) self.toast_overlay.add_toast(Adw.Toast(title=response))
GLib.idle_add(lambda *_: self.pin_switch.set_active(state)) GLib.idle_add(lambda *_: self.pin_switch.set_active(state))
self.package.app_row.pinned_status_icon.set_visible(state) self.package.app_row.pinned_status_icon.set_visible(state)
self.package.set_pin(state, callback) self.package.set_pin(state, callback)
def uninstall_handler(self, *args): def uninstall_handler(self, *args):
def on_choice(should_trash): def on_choice(should_trash):
self.packages_page.set_status(self.packages_page.uninstalling) self.packages_page.set_status(self.packages_page.uninstalling)
@@ -238,7 +238,7 @@ class PropertiesPage(Adw.NavigationPage):
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), cpe.stderr).toast)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(e)).toast)
def callback(*args): def callback(*args):
if fail := self.package.failed_uninstall: if fail := self.package.failed_uninstall:
fail = fail.stderr if type(fail) is subprocess.CalledProcessError else fail fail = fail.stderr if type(fail) is subprocess.CalledProcessError else fail
@@ -247,48 +247,48 @@ class PropertiesPage(Adw.NavigationPage):
else: else:
self.main_window.refresh_handler() self.main_window.refresh_handler()
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled {}").format(self.package.info["name"]))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled {}").format(self.package.info["name"])))
dialog = UninstallDialog(on_choice, os.path.exists(self.package.data_path), self.package.info["name"]) dialog = UninstallDialog(on_choice, os.path.exists(self.package.data_path), self.package.info["name"])
dialog.present(self.main_window) dialog.present(self.main_window)
def runtime_row_handler(self, *args): def runtime_row_handler(self, *args):
new_page = self.__class__() new_page = self.__class__()
new_page.packages_page = self.packages_page new_page.packages_page = self.packages_page
new_page.set_properties(self.package.dependent_runtime) new_page.set_properties(self.package.dependent_runtime)
self.nav_view.push(new_page) self.nav_view.push(new_page)
def open_app_handler(self, *args): def open_app_handler(self, *args):
self.toast_overlay.add_toast(Adw.Toast(title=_("Opening {}").format(self.package.info["name"]))) self.toast_overlay.add_toast(Adw.Toast(title=_("Opening {}").format(self.package.info["name"])))
def callback(*args): def callback(*args):
if fail := self.package.failed_app_run: if fail := self.package.failed_app_run:
fail = fail.stderr if type(fail) is subprocess.CalledProcessError else fail fail = fail.stderr if type(fail) is subprocess.CalledProcessError else fail
self.toast_overlay.add_toast(ErrorToast(_("Could not open {}").format(self.package.info["name"]), str(fail)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open {}").format(self.package.info["name"]), str(fail)).toast)
self.package.open_app(callback) self.package.open_app(callback)
def copy_handler(self, row): def copy_handler(self, row):
HostInfo.clipboard.set(row.get_subtitle()) HostInfo.clipboard.set(row.get_subtitle())
self.toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(row.get_title()))) self.toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(row.get_title())))
def change_version_handler(self, row): def change_version_handler(self, row):
page = ChangeVersionPage(self.packages_page, self.package) page = ChangeVersionPage(self.packages_page, self.package)
self.nav_view.push(page) self.nav_view.push(page)
def reinstall_callback(self): def reinstall_callback(self):
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
if not self.reinstall_did_error: if not self.reinstall_did_error:
HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Reinstalled {}").format(self.package.info['name']))) HostInfo.main_window.toast_overlay.add_toast(Adw.Toast(title=_("Reinstalled {}").format(self.package.info['name'])))
def reinstall_error_callback(self, user_facing_label, error_message): def reinstall_error_callback(self, user_facing_label, error_message):
self.reinstall_did_error = True self.reinstall_did_error = True
GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)) GLib.idle_add(lambda *_: HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast))
def reinstall_handler(self): def reinstall_handler(self):
def on_response(dialog, response): def on_response(dialog, response):
if response != "continue": if response != "continue":
return return
self.reinstall_did_error = False self.reinstall_did_error = False
PackageInstallWorker.install( PackageInstallWorker.install(
[{ [{
@@ -302,7 +302,7 @@ class PropertiesPage(Adw.NavigationPage):
self.reinstall_error_callback, self.reinstall_error_callback,
) )
self.packages_page.set_status(self.packages_page.reinstalling) self.packages_page.set_status(self.packages_page.reinstalling)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Reinstall {}?").format(self.package.info['name']), heading=_("Reinstall {}?").format(self.package.info['name']),
body=_("This package will be uninstalled, and then reinstalled from the same remote and installation.") body=_("This package will be uninstalled, and then reinstalled from the same remote and installation.")
@@ -312,7 +312,7 @@ class PropertiesPage(Adw.NavigationPage):
dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED) dialog.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def more_menu_handler(self, listbox, row): def more_menu_handler(self, listbox, row):
self.more_menu.popdown() self.more_menu.popdown()
match row.get_child(): match row.get_child():
@@ -321,43 +321,43 @@ class PropertiesPage(Adw.NavigationPage):
snapshots_page = HostInfo.main_window.pages[snapshots_row] snapshots_page = HostInfo.main_window.pages[snapshots_row]
HostInfo.main_window.activate_row(snapshots_row) HostInfo.main_window.activate_row(snapshots_row)
snapshots_page.show_snapshot(self.package) snapshots_page.show_snapshot(self.package)
case self.copy_launch_command: case self.copy_launch_command:
try: try:
HostInfo.clipboard.set(f"flatpak run {self.package.info['ref']}") HostInfo.clipboard.set(f"flatpak run {self.package.info['ref']}")
self.toast_overlay.add_toast(Adw.Toast.new(_("Copied launch command"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Copied launch command")))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not copy launch command"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not copy launch command"), str(e)).toast)
case self.show_details: case self.show_details:
try: try:
Gio.AppInfo.launch_default_for_uri(f"appstream://{self.package.info['id']}", None) Gio.AppInfo.launch_default_for_uri(f"appstream://{self.package.info['id']}", None)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not show details"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not show details"), str(e)).toast)
case self.reinstall: case self.reinstall:
self.reinstall_handler() self.reinstall_handler()
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.main_window = HostInfo.main_window self.main_window = HostInfo.main_window
self.info_rows = { self.info_rows = {
"version": self.version_row, "version": self.version_row,
"installed": self.installed_size_row, "installed": self.installed_size_row,
"id": self.id_row, "id": self.id_row,
"ref": self.ref_row, "ref": self.ref_row,
"arch": self.arch_row, "arch": self.arch_row,
"branch": self.branch_row, "branch": self.branch_row,
"license": self.license_row, "license": self.license_row,
"sdk": self.sdk_row, "sdk": self.sdk_row,
"origin": self.origin_row, "origin": self.origin_row,
"collection": self.collection_row, "collection": self.collection_row,
"installation": self.installation_row, "installation": self.installation_row,
"commit": self.commit_row, "commit": self.commit_row,
"parent": self.parent_row, "parent": self.parent_row,
"subject": self.subject_row, "subject": self.subject_row,
@@ -371,9 +371,9 @@ class PropertiesPage(Adw.NavigationPage):
self.show_details = Gtk.Label(halign=Gtk.Align.START, label=_("Show Details")) self.show_details = Gtk.Label(halign=Gtk.Align.START, label=_("Show Details"))
self.reinstall = Gtk.Label(halign=Gtk.Align.START, label=_("Reinstall")) self.reinstall = Gtk.Label(halign=Gtk.Align.START, label=_("Reinstall"))
self.reinstall_did_error = False self.reinstall_did_error = False
# Apply # Apply
# Connections # Connections
self.more_list.connect("row-activated", self.more_menu_handler) self.more_list.connect("row-activated", self.more_menu_handler)
self.open_data_button.connect("clicked", self.open_data_handler) self.open_data_button.connect("clicked", self.open_data_handler)

View File

@@ -9,7 +9,7 @@ import subprocess, re
class AddRemoteDialog(Adw.Dialog): class AddRemoteDialog(Adw.Dialog):
__gtype_name__ = "AddRemoteDialog" __gtype_name__ = "AddRemoteDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toast_overlay = gtc() toast_overlay = gtc()
cancel_button = gtc() cancel_button = gtc()
apply_button = gtc() apply_button = gtc()
@@ -19,7 +19,7 @@ class AddRemoteDialog(Adw.Dialog):
url_row = gtc() url_row = gtc()
installation_chooser = gtc() installation_chooser = gtc()
is_open = False is_open = False
def on_apply(self, *args): def on_apply(self, *args):
self.parent_page.status_stack.set_visible_child(self.parent_page.adding_view) self.parent_page.status_stack.set_visible_child(self.parent_page.adding_view)
self.apply_button.set_sensitive(False) self.apply_button.set_sensitive(False)
@@ -38,14 +38,14 @@ class AddRemoteDialog(Adw.Dialog):
cmd.append(f"--{installation}") cmd.append(f"--{installation}")
else: else:
cmd.append(f"--installation={installation}") cmd.append(f"--installation={installation}")
try: try:
subprocess.run(cmd, check=True, capture_output=True, text=True) subprocess.run(cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = e error[0] = e
def callback(*args): def callback(*args):
HostInfo.main_window.remove_refresh_lockout("adding remote") HostInfo.main_window.remove_refresh_lockout("adding remote")
if error[0]: if error[0]:
@@ -55,17 +55,17 @@ class AddRemoteDialog(Adw.Dialog):
else: else:
self.main_window.refresh_handler() self.main_window.refresh_handler()
self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Added {}").format(self.title_row.get_text()))) self.parent_page.toast_overlay.add_toast(Adw.Toast(title=_("Added {}").format(self.title_row.get_text())))
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
self.close() self.close()
def check_entries(self, row): def check_entries(self, row):
is_passing = re.match(self.rexes[row], row.get_text()) is_passing = re.match(self.rexes[row], row.get_text())
if is_passing: if is_passing:
row.remove_css_class("error") row.remove_css_class("error")
else: else:
row.add_css_class("error") row.add_css_class("error")
match row: match row:
case self.title_row: case self.title_row:
self.title_passes = bool(is_passing) self.title_passes = bool(is_passing)
@@ -73,27 +73,27 @@ class AddRemoteDialog(Adw.Dialog):
self.name_passes = bool(is_passing) self.name_passes = bool(is_passing)
case self.url_row: case self.url_row:
self.url_passes = bool(is_passing) self.url_passes = bool(is_passing)
self.apply_button.set_sensitive(self.title_passes and self.name_passes and self.url_passes) self.apply_button.set_sensitive(self.title_passes and self.name_passes and self.url_passes)
def present(self, *args, **kwargs): def present(self, *args, **kwargs):
if self.__class__.is_open: if self.__class__.is_open:
return return
self.__class__.is_open = True self.__class__.is_open = True
super().present(*args, **kwargs) super().present(*args, **kwargs)
def on_close(self, *args): def on_close(self, *args):
self.__class__.is_open = False self.__class__.is_open = False
def __init__(self, main_window, parent_page, remote_info=None, **kwargs): def __init__(self, main_window, parent_page, remote_info=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.string_list = Gtk.StringList(strings=HostInfo.installations) self.string_list = Gtk.StringList(strings=HostInfo.installations)
self.main_window = main_window self.main_window = main_window
self.parent_page = parent_page self.parent_page = parent_page
self.rexes = { self.rexes = {
self.title_row: r"^(?=.*[A-Za-z0-9])[A-Za-z0-9._-]+( +[A-Za-z0-9._-]+)*$", self.title_row: r"^(?=.*[A-Za-z0-9])[A-Za-z0-9._-]+( +[A-Za-z0-9._-]+)*$",
self.name_row: r"^[a-zA-Z0-9\-._]+$", self.name_row: r"^[a-zA-Z0-9\-._]+$",
@@ -102,7 +102,7 @@ class AddRemoteDialog(Adw.Dialog):
self.title_passes = False self.title_passes = False
self.name_passes = False self.name_passes = False
self.url_passes = False self.url_passes = False
# Apply # Apply
self.installation_chooser.set_content_strings(_("remote"), False) self.installation_chooser.set_content_strings(_("remote"), False)
if remote_info: if remote_info:
@@ -121,7 +121,7 @@ class AddRemoteDialog(Adw.Dialog):
self.apply_button.set_sensitive(True) self.apply_button.set_sensitive(True)
else: else:
self.apply_button.set_sensitive(False) self.apply_button.set_sensitive(False)
# Connections # Connections
self.connect("closed", self.on_close) self.connect("closed", self.on_close)
self.cancel_button.connect("clicked", lambda *_: self.close()) self.cancel_button.connect("clicked", lambda *_: self.close())

View File

@@ -43,7 +43,7 @@ class RemoteRow(Adw.ActionRow):
if len(has_error) > 0: if len(has_error) > 0:
GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), has_error[0]).toast)) GLib.idle_add(lambda *args, cpe=cpe: self.parent_page.toast_overlay.add_toast(ErrorToast(_("Could not enable remote"), has_error[0]).toast))
return return
self.remove_css_class("warning") self.remove_css_class("warning")
self.set_icon_name("") self.set_icon_name("")
self.set_tooltip_text("") self.set_tooltip_text("")
@@ -60,7 +60,7 @@ class RemoteRow(Adw.ActionRow):
if self.parent_page.total_disabled == 0: if self.parent_page.total_disabled == 0:
self.parent_page.show_disabled_button.set_active(False) self.parent_page.show_disabled_button.set_active(False)
self.parent_page.show_disabled_button.set_visible(False) self.parent_page.show_disabled_button.set_visible(False)
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
def disable_remote_handler(self, *args): def disable_remote_handler(self, *args):
@@ -115,7 +115,7 @@ class RemoteRow(Adw.ActionRow):
def on_response(_, response): def on_response(_, response):
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = Adw.AlertDialog(heading=_("Disable {}?").format(self.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(self.remote.name)) dialog = Adw.AlertDialog(heading=_("Disable {}?").format(self.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(self.remote.name))

View File

@@ -22,7 +22,6 @@ class NewRemoteRow(Adw.ActionRow):
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui")
class RemotesPage(Adw.NavigationPage): class RemotesPage(Adw.NavigationPage):
# Preselected Remotes # Preselected Remotes
new_remotes = [ new_remotes = [
{ {
@@ -92,7 +91,7 @@ class RemotesPage(Adw.NavigationPage):
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
instance = None instance = None
page_name = "remotes" page_name = "remotes"
def start_loading(self): def start_loading(self):
self.search_button.set_active(False) self.search_button.set_active(False)
self.status_stack.set_visible_child(self.loading_view) self.status_stack.set_visible_child(self.loading_view)
@@ -137,7 +136,7 @@ class RemotesPage(Adw.NavigationPage):
if row.get_visible(): if row.get_visible():
any_visible = True any_visible = True
break break
self.none_visible.set_visible(not any_visible) self.none_visible.set_visible(not any_visible)
def filter_remote(self, row): def filter_remote(self, row):
@@ -150,7 +149,7 @@ class RemotesPage(Adw.NavigationPage):
packages_page.apply_filters() packages_page.apply_filters()
GLib.idle_add(lambda *_: self.main_window.activate_row(self.main_window.packages_row)) GLib.idle_add(lambda *_: self.main_window.activate_row(self.main_window.packages_row))
GLib.idle_add(lambda *args: packages_page.packages_toast_overlay.add_toast(Adw.Toast(title=_("Showing all packages from {}").format(row.remote.title)))) GLib.idle_add(lambda *args: packages_page.packages_toast_overlay.add_toast(Adw.Toast(title=_("Showing all packages from {}").format(row.remote.title))))
def remove_remote(self, row): def remove_remote(self, row):
error = [None] error = [None]
def thread(*args): def thread(*args):
@@ -182,7 +181,7 @@ class RemotesPage(Adw.NavigationPage):
def on_response(_, response): def on_response(_, response):
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
dialog = Adw.AlertDialog(heading=_("Remove {}?").format(row.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(row.remote.name)) dialog = Adw.AlertDialog(heading=_("Remove {}?").format(row.remote.title), body=_("Any installed apps from {} will stop receiving updates").format(row.remote.name))
@@ -196,7 +195,7 @@ class RemotesPage(Adw.NavigationPage):
text = entry.get_text().lower() text = entry.get_text().lower()
total = 0 total = 0
show_disabled = self.show_disabled_button.get_active() show_disabled = self.show_disabled_button.get_active()
for row in self.current_remote_rows: for row in self.current_remote_rows:
title_match = text in row.get_title().lower() title_match = text in row.get_title().lower()
subtitle_match = text in row.get_subtitle().lower() subtitle_match = text in row.get_subtitle().lower()
@@ -209,7 +208,7 @@ class RemotesPage(Adw.NavigationPage):
return return
self.stack.set_visible_child(self.content_page if total > 0 else self.no_results) self.stack.set_visible_child(self.content_page if total > 0 else self.no_results)
def local_file_handler(self, path): def local_file_handler(self, path):
try: try:
name = path.split("/")[-1].split(".")[0] name = path.split("/")[-1].split(".")[0]
@@ -260,7 +259,7 @@ class RemotesPage(Adw.NavigationPage):
total_visible += 1 total_visible += 1
self.none_visible.set_visible(total_visible == 0) self.none_visible.set_visible(total_visible == 0)
def new_custom_handler(self, *args): def new_custom_handler(self, *args):
AddRemoteDialog(self.main_window, self).present(self.main_window) AddRemoteDialog(self.main_window, self).present(self.main_window)

View File

@@ -9,7 +9,7 @@ import os, time
class NewSnapshotDialog(Adw.Dialog): class NewSnapshotDialog(Adw.Dialog):
__gtype_name__ = "NewSnapshotDialog" __gtype_name__ = "NewSnapshotDialog"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toast_overlay = gtc() toast_overlay = gtc()
nav_page = gtc() nav_page = gtc()
list_cancel_button = gtc() list_cancel_button = gtc()
@@ -24,39 +24,39 @@ class NewSnapshotDialog(Adw.Dialog):
no_results = gtc() no_results = gtc()
stack = gtc() stack = gtc()
is_open = False is_open = False
def row_gesture_handler(self, row): def row_gesture_handler(self, row):
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
def row_select_handler(self, row): def row_select_handler(self, row):
if row.check_button.get_active(): if row.check_button.get_active():
self.selected_rows.append(row) self.selected_rows.append(row)
else: else:
self.selected_rows.remove(row) self.selected_rows.remove(row)
total = len(self.selected_rows) total = len(self.selected_rows)
self.total_selected_label.set_label(_("{} Selected").format(total)) self.total_selected_label.set_label(_("{} Selected").format(total))
self.total_selected_label.set_visible(total > 0) self.total_selected_label.set_visible(total > 0)
self.valid_checker() self.valid_checker()
def generate_list(self, *args): def generate_list(self, *args):
for package in HostInfo.flatpaks: for package in HostInfo.flatpaks:
if "io.github.flattool.Warehouse" in package.info["id"]: if "io.github.flattool.Warehouse" in package.info["id"]:
continue continue
if package.is_runtime or not os.path.exists(package.data_path): if package.is_runtime or not os.path.exists(package.data_path):
continue continue
row = AppRow(package, self.row_gesture_handler) row = AppRow(package, self.row_gesture_handler)
row.check_button.set_visible(True) row.check_button.set_visible(True)
row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row)) row.check_button.connect("toggled", lambda *_, row=row: self.row_select_handler(row))
row.set_activatable(True) row.set_activatable(True)
row.set_activatable_widget(row.check_button) row.set_activatable_widget(row.check_button)
self.listbox.append(row) self.listbox.append(row)
def sort_func(self, row1, row2): def sort_func(self, row1, row2):
return row1.package.info["name"].lower() > row2.package.info["name"].lower() return row1.package.info["name"].lower() > row2.package.info["name"].lower()
def filter_func(self, row): def filter_func(self, row):
title = row.get_title().lower() title = row.get_title().lower()
subtitle = row.get_subtitle().lower() subtitle = row.get_subtitle().lower()
@@ -66,13 +66,13 @@ class NewSnapshotDialog(Adw.Dialog):
return True return True
else: else:
return False return False
def on_close(self, *args): def on_close(self, *args):
self.__class__.is_open = False self.__class__.is_open = False
self.search_button.set_active(False) self.search_button.set_active(False)
for row in self.selected_rows.copy(): for row in self.selected_rows.copy():
GLib.idle_add(lambda *_, row=row: row.check_button.set_active(False)) GLib.idle_add(lambda *_, row=row: row.check_button.set_active(False))
def valid_checker(self): def valid_checker(self):
text = self.name_entry.get_text().strip() text = self.name_entry.get_text().strip()
something_selected = len(self.selected_rows) > 0 something_selected = len(self.selected_rows) > 0
@@ -82,9 +82,9 @@ class NewSnapshotDialog(Adw.Dialog):
self.name_entry.remove_css_class("error") self.name_entry.remove_css_class("error")
else: else:
self.name_entry.add_css_class("error") self.name_entry.add_css_class("error")
return something_selected and text_good return something_selected and text_good
def get_total_fraction(self): def get_total_fraction(self):
total = 0 total = 0
stopped_workers_amount = 0 stopped_workers_amount = 0
@@ -92,20 +92,20 @@ class NewSnapshotDialog(Adw.Dialog):
total += worker.fraction total += worker.fraction
if worker.stop: if worker.stop:
stopped_workers_amount += 1 stopped_workers_amount += 1
if stopped_workers_amount == len(self.workers): if stopped_workers_amount == len(self.workers):
self.loading_status.progress_bar.set_fraction(1) self.loading_status.progress_bar.set_fraction(1)
self.loading_status.progress_label.set_label(f"{len(self.workers)} / {len(self.workers)}") self.loading_status.progress_label.set_label(f"{len(self.workers)} / {len(self.workers)}")
self.workers.clear() self.workers.clear()
if self.on_done: if self.on_done:
self.on_done() self.on_done()
return False return False
self.loading_status.progress_label.set_label(f"{stopped_workers_amount + 1} / {len(self.workers)}") self.loading_status.progress_label.set_label(f"{stopped_workers_amount + 1} / {len(self.workers)}")
self.loading_status.progress_bar.set_fraction(total / len(self.workers)) self.loading_status.progress_bar.set_fraction(total / len(self.workers))
return True return True
def on_create(self, button): def on_create(self, button):
self.loading_status.title_label.set_label(_("Creating Snapshot")) self.loading_status.title_label.set_label(_("Creating Snapshot"))
self.loading_status.progress_bar.set_fraction(0.0) self.loading_status.progress_bar.set_fraction(0.0)
@@ -114,7 +114,7 @@ class NewSnapshotDialog(Adw.Dialog):
for row in self.selected_rows: for row in self.selected_rows:
if "io.github.flattool.Warehouse" in row.package.info["id"]: if "io.github.flattool.Warehouse" in row.package.info["id"]:
continue continue
package = row.package package = row.package
worker = TarWorker( worker = TarWorker(
existing_path=package.data_path, existing_path=package.data_path,
@@ -125,11 +125,11 @@ class NewSnapshotDialog(Adw.Dialog):
) )
self.workers.append(worker) self.workers.append(worker)
worker.compress() worker.compress()
self.loading_status.progress_label.set_visible(len(self.workers) > 1) self.loading_status.progress_label.set_visible(len(self.workers) > 1)
GLib.timeout_add(200, self.get_total_fraction) GLib.timeout_add(200, self.get_total_fraction)
self.close() self.close()
def on_invalidate(self, search_entry): def on_invalidate(self, search_entry):
self.is_result = False self.is_result = False
self.listbox.invalidate_filter() self.listbox.invalidate_filter()
@@ -137,36 +137,36 @@ class NewSnapshotDialog(Adw.Dialog):
self.stack.set_visible_child(self.scrolled_window) self.stack.set_visible_child(self.scrolled_window)
else: else:
self.stack.set_visible_child(self.no_results) self.stack.set_visible_child(self.no_results)
def on_select_all(self, button): def on_select_all(self, button):
i = 0 i = 0
while row := self.listbox.get_row_at_index(i): while row := self.listbox.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_active(True) row.check_button.set_active(True)
def set_packages(self): def set_packages(self):
for package in self.packages: for package in self.packages:
row = AppRow(package) row = AppRow(package)
row.set_activatable(False) row.set_activatable(False)
self.selected_rows.append(row) self.selected_rows.append(row)
self.listbox.append(row) self.listbox.append(row)
def enter_handler(self, *args): def enter_handler(self, *args):
if self.create_button.get_sensitive(): if self.create_button.get_sensitive():
self.create_button.activate() self.create_button.activate()
def present(self, *args, **kwargs): def present(self, *args, **kwargs):
if self.__class__.is_open: if self.__class__.is_open:
return return
super().present(*args, **kwargs) super().present(*args, **kwargs)
self.__class__.is_open = True self.__class__.is_open = True
if not self.search_button.get_visible(): if not self.search_button.get_visible():
self.name_entry.grab_focus() self.name_entry.grab_focus()
def __init__(self, snapshot_page, loading_status, on_done=None, packages=None, **kwargs): def __init__(self, snapshot_page, loading_status, on_done=None, packages=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creations # Extra Object Creations
self.snapshot_page = snapshot_page self.snapshot_page = snapshot_page
self.loading_status = loading_status self.loading_status = loading_status
@@ -176,7 +176,7 @@ class NewSnapshotDialog(Adw.Dialog):
self.selected_rows = [] self.selected_rows = []
self.workers = [] self.workers = []
self.packages = packages self.packages = packages
# Connections # Connections
self.connect("closed", self.on_close) self.connect("closed", self.on_close)
self.create_button.connect("clicked", self.on_create) self.create_button.connect("clicked", self.on_create)
@@ -185,7 +185,7 @@ class NewSnapshotDialog(Adw.Dialog):
self.name_entry.connect("changed", lambda *_: self.valid_checker()) self.name_entry.connect("changed", lambda *_: self.valid_checker())
self.name_entry.connect("entry-activated", self.enter_handler) self.name_entry.connect("entry-activated", self.enter_handler)
self.select_all_button.connect("clicked", self.on_select_all) self.select_all_button.connect("clicked", self.on_select_all)
# Apply # Apply
self.listbox.set_sort_func(self.sort_func) self.listbox.set_sort_func(self.sort_func)
self.listbox.set_filter_func(self.filter_func) self.listbox.set_filter_func(self.filter_func)

View File

@@ -8,7 +8,7 @@ import os, subprocess, json
class SnapshotBox(Gtk.Box): class SnapshotBox(Gtk.Box):
__gtype_name__ = "SnapshotBox" __gtype_name__ = "SnapshotBox"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
title = gtc() title = gtc()
date = gtc() date = gtc()
version = gtc() version = gtc()
@@ -18,7 +18,7 @@ class SnapshotBox(Gtk.Box):
rename_entry = gtc() rename_entry = gtc()
apply_rename = gtc() apply_rename = gtc()
trash_button = gtc() trash_button = gtc()
def create_json(self): def create_json(self):
try: try:
data = { data = {
@@ -28,10 +28,10 @@ class SnapshotBox(Gtk.Box):
with open(self.json_path, 'w') as file: with open(self.json_path, 'w') as file:
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
return None return None
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def update_json(self, key, value): def update_json(self, key, value):
try: try:
with open(self.json_path, 'r+') as file: with open(self.json_path, 'r+') as file:
@@ -40,14 +40,14 @@ class SnapshotBox(Gtk.Box):
file.seek(0) file.seek(0)
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
file.truncate() file.truncate()
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def load_from_json(self): def load_from_json(self):
if not os.path.exists(self.json_path): if not os.path.exists(self.json_path):
self.create_json() self.create_json()
try: try:
with open(self.json_path, 'r') as file: with open(self.json_path, 'r') as file:
data = json.load(file) data = json.load(file)
@@ -56,18 +56,18 @@ class SnapshotBox(Gtk.Box):
self.title.set_label(GLib.markup_escape_text(name)) self.title.set_label(GLib.markup_escape_text(name))
else: else:
self.title.set_label(_("No Name Set")) self.title.set_label(_("No Name Set"))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast)
def on_rename(self, widget): def on_rename(self, widget):
if not self.valid_checker(): if not self.valid_checker():
return return
self.update_json('name', self.rename_entry.get_text().strip()) self.update_json('name', self.rename_entry.get_text().strip())
self.load_from_json() self.load_from_json()
self.rename_menu.popdown() self.rename_menu.popdown()
def valid_checker(self, *args): def valid_checker(self, *args):
text = self.rename_entry.get_text().strip() text = self.rename_entry.get_text().strip()
valid = not ("/" in text or "\0" in text) and len(text) > 0 valid = not ("/" in text or "\0" in text) and len(text) > 0
@@ -76,15 +76,15 @@ class SnapshotBox(Gtk.Box):
self.rename_entry.remove_css_class("error") self.rename_entry.remove_css_class("error")
else: else:
self.rename_entry.add_css_class("error") self.rename_entry.add_css_class("error")
return valid return valid
def on_trash(self, button): def on_trash(self, button):
error = [None] error = [None]
path = f"{self.snapshots_path}{self.folder}" path = f"{self.snapshots_path}{self.folder}"
if self.snapshot_page.is_trash_dialog_open: if self.snapshot_page.is_trash_dialog_open:
return return
def thread(*args): def thread(*args):
try: try:
subprocess.run(['gio', 'trash', path], capture_output=True, text=True, check=True) subprocess.run(['gio', 'trash', path], capture_output=True, text=True, check=True)
@@ -92,22 +92,22 @@ class SnapshotBox(Gtk.Box):
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = str(e) error[0] = str(e)
def callback(*args): def callback(*args):
if not error[0] is None: if not error[0] is None:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshot"), error[0]).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshot"), error[0]).toast)
return return
self.parent_page.on_trash() self.parent_page.on_trash()
self.toast_overlay.add_toast(Adw.Toast.new(_("Trashed snapshot"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Trashed snapshot")))
def on_response(_, response): def on_response(_, response):
self.snapshot_page.is_trash_dialog_open = False self.snapshot_page.is_trash_dialog_open = False
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
self.snapshot_page.is_trash_dialog_open = True self.snapshot_page.is_trash_dialog_open = True
dialog = Adw.AlertDialog(heading=_("Trash Snapshot?"), body=_("This snapshot will be sent to the trash")) dialog = Adw.AlertDialog(heading=_("Trash Snapshot?"), body=_("This snapshot will be sent to the trash"))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@@ -115,7 +115,7 @@ class SnapshotBox(Gtk.Box):
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def get_fraction(self): def get_fraction(self):
loading_status = self.snapshot_page.snapshotting_status loading_status = self.snapshot_page.snapshotting_status
loading_status.progress_bar.set_fraction(self.worker.fraction) loading_status.progress_bar.set_fraction(self.worker.fraction)
@@ -129,16 +129,16 @@ class SnapshotBox(Gtk.Box):
data_page.end_loading() data_page.end_loading()
if self.worker in self.snapshot_page.workers: if self.worker in self.snapshot_page.workers:
self.snapshot_page.workers.remove(self.worker) self.snapshot_page.workers.remove(self.worker)
return False # Stop the timeout return False # Stop the timeout
else: else:
return True # Continue the timeout return True # Continue the timeout
def on_apply(self, button): def on_apply(self, button):
def on_response(dialog, response): def on_response(dialog, response):
if response != "continue": if response != "continue":
return return
self.snapshot_page.snapshotting_status.title_label.set_label(_("Applying Snapshot")) self.snapshot_page.snapshotting_status.title_label.set_label(_("Applying Snapshot"))
self.snapshot_page.snapshotting_status.progress_label.set_visible(False) self.snapshot_page.snapshotting_status.progress_label.set_visible(False)
self.snapshot_page.snapshotting_status.progress_bar.set_fraction(0.0) self.snapshot_page.snapshotting_status.progress_bar.set_fraction(0.0)
@@ -146,7 +146,7 @@ class SnapshotBox(Gtk.Box):
self.snapshot_page.workers.append(self.worker) self.snapshot_page.workers.append(self.worker)
self.worker.extract() self.worker.extract()
GLib.timeout_add(200, self.get_fraction) GLib.timeout_add(200, self.get_fraction)
has_data = os.path.exists(self.worker.new_path) has_data = os.path.exists(self.worker.new_path)
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Apply Snapshot?"), heading=_("Apply Snapshot?"),
@@ -156,10 +156,10 @@ class SnapshotBox(Gtk.Box):
dialog.add_response("continue", _("Apply")) dialog.add_response("continue", _("Apply"))
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def __init__(self, parent_page, folder, snapshots_path, toast_overlay, **kwargs): def __init__(self, parent_page, folder, snapshots_path, toast_overlay, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.snapshot_page = parent_page.parent_page self.snapshot_page = parent_page.parent_page
self.toast_overlay = toast_overlay self.toast_overlay = toast_overlay
self.app_id = snapshots_path.split('/')[-2].strip() self.app_id = snapshots_path.split('/')[-2].strip()
@@ -168,11 +168,11 @@ class SnapshotBox(Gtk.Box):
new_path=f"{HostInfo.home}/.var/app/{self.app_id}/", new_path=f"{HostInfo.home}/.var/app/{self.app_id}/",
toast_overlay=self.toast_overlay, toast_overlay=self.toast_overlay,
) )
split_folder = folder.split('_') split_folder = folder.split('_')
if len(split_folder) < 2: if len(split_folder) < 2:
return return
self.parent_page = parent_page self.parent_page = parent_page
self.folder = folder self.folder = folder
self.snapshots_path = snapshots_path self.snapshots_path = snapshots_path

View File

@@ -62,10 +62,10 @@ template $SnapshotPage : Adw.BreakpointBin {
ScrolledWindow scrolled_window { ScrolledWindow scrolled_window {
Box { Box {
orientation: vertical; orientation: vertical;
Box active_box { Box active_box {
orientation: vertical; orientation: vertical;
Label { Label {
label: _("Active Snapshots"); label: _("Active Snapshots");
halign: start; halign: start;
@@ -94,7 +94,7 @@ template $SnapshotPage : Adw.BreakpointBin {
} }
Box leftover_box { Box leftover_box {
orientation: vertical; orientation: vertical;
Label { Label {
label: _("Leftover Snapshots"); label: _("Leftover Snapshots");
halign: start; halign: start;

View File

@@ -12,27 +12,27 @@ import os, subprocess
class LeftoverSnapshotRow(Adw.ActionRow): class LeftoverSnapshotRow(Adw.ActionRow):
__gtype_name__ = "LeftoverSnapshotRow" __gtype_name__ = "LeftoverSnapshotRow"
def idle_stuff(self): def idle_stuff(self):
self.set_title(self.name) self.set_title(self.name)
icon = Gtk.Image.new_from_icon_name("application-x-executable-symbolic") icon = Gtk.Image.new_from_icon_name("application-x-executable-symbolic")
icon.set_icon_size(Gtk.IconSize.LARGE) icon.set_icon_size(Gtk.IconSize.LARGE)
self.add_prefix(icon) self.add_prefix(icon)
self.add_suffix(self.check_button) self.add_suffix(self.check_button)
def gesture_handler(self, *args): def gesture_handler(self, *args):
self.on_long_press(self) self.on_long_press(self)
def __init__(self, folder, on_long_press, **kwargs): def __init__(self, folder, on_long_press, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.folder = folder self.folder = folder
self.check_button = Gtk.CheckButton(visible=False) self.check_button = Gtk.CheckButton(visible=False)
self.on_long_press = on_long_press self.on_long_press = on_long_press
self.rclick_gesture = Gtk.GestureClick(button=3) self.rclick_gesture = Gtk.GestureClick(button=3)
self.long_press_gesture = Gtk.GestureLongPress() self.long_press_gesture = Gtk.GestureLongPress()
# Apply # Apply
self.add_controller(self.rclick_gesture) self.add_controller(self.rclick_gesture)
self.add_controller(self.long_press_gesture) self.add_controller(self.long_press_gesture)
@@ -40,16 +40,16 @@ class LeftoverSnapshotRow(Adw.ActionRow):
self.name = self.folder.split('.')[-1] self.name = self.folder.split('.')[-1]
self.set_activatable(True) self.set_activatable(True)
GLib.idle_add(lambda *_: self.idle_stuff()) GLib.idle_add(lambda *_: self.idle_stuff())
# Connections # Connections
self.rclick_gesture.connect("released", self.gesture_handler) self.rclick_gesture.connect("released", self.gesture_handler)
self.long_press_gesture.connect("pressed", self.gesture_handler) self.long_press_gesture.connect("pressed", self.gesture_handler)
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_page.ui") @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_page.ui")
class SnapshotPage(Adw.BreakpointBin): class SnapshotPage(Adw.BreakpointBin):
__gtype_name__ = "SnapshotPage" __gtype_name__ = "SnapshotPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toast_overlay = gtc() toast_overlay = gtc()
sidebar_navpage = gtc() sidebar_navpage = gtc()
search_button = gtc() search_button = gtc()
@@ -80,7 +80,7 @@ class SnapshotPage(Adw.BreakpointBin):
apply_snapshots = gtc() apply_snapshots = gtc()
install_from_snapshots = gtc() install_from_snapshots = gtc()
trash_snapshots = gtc() trash_snapshots = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
@@ -88,94 +88,94 @@ class SnapshotPage(Adw.BreakpointBin):
page_name = "snapshots" page_name = "snapshots"
is_trash_dialog_open = False is_trash_dialog_open = False
last_activated_row = None last_activated_row = None
def sort_snapshots(self, *args): def sort_snapshots(self, *args):
self.active_snapshot_paks.clear() self.active_snapshot_paks.clear()
self.leftover_snapshots.clear() self.leftover_snapshots.clear()
bad_folders = [] bad_folders = []
if not os.path.exists(HostInfo.snapshots_path): if not os.path.exists(HostInfo.snapshots_path):
try: try:
os.makedirs(HostInfo.snapshots_path) os.makedirs(HostInfo.snapshots_path)
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not load Snapshots"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not load Snapshots"), str(e)).toast)
return return
for folder in os.listdir(HostInfo.snapshots_path): for folder in os.listdir(HostInfo.snapshots_path):
if folder.count('.') < 2 or ' ' in folder: if folder.count('.') < 2 or ' ' in folder:
bad_folders.append(folder) bad_folders.append(folder)
continue continue
has_tar = False has_tar = False
for file in os.listdir(f"{HostInfo.snapshots_path}{folder}"): for file in os.listdir(f"{HostInfo.snapshots_path}{folder}"):
if file.endswith(".tar.zst"): if file.endswith(".tar.zst"):
has_tar = True has_tar = True
break break
if not has_tar: if not has_tar:
bad_folders.append(folder) bad_folders.append(folder)
continue continue
try: try:
pak = HostInfo.id_to_flatpak[folder] pak = HostInfo.id_to_flatpak[folder]
self.active_snapshot_paks.append(pak) self.active_snapshot_paks.append(pak)
except KeyError: except KeyError:
self.leftover_snapshots.append(folder) self.leftover_snapshots.append(folder)
for folder in bad_folders: for folder in bad_folders:
try: try:
subprocess.run(['gio', 'trash', f'{HostInfo.snapshots_path}{folder}']) subprocess.run(['gio', 'trash', f'{HostInfo.snapshots_path}{folder}'])
except Exception: except Exception:
pass pass
def long_press_handler(self, row): def long_press_handler(self, row):
self.select_button.set_active(True) self.select_button.set_active(True)
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
def generate_active_list(self): def generate_active_list(self):
for pak in self.active_snapshot_paks: for pak in self.active_snapshot_paks:
row = AppRow(pak, self.long_press_handler) row = AppRow(pak, self.long_press_handler)
row.check_button.connect("toggled", lambda *_, _row=row: self.row_select_handler(_row)) row.check_button.connect("toggled", lambda *_, _row=row: self.row_select_handler(_row))
self.active_listbox.append(row) self.active_listbox.append(row)
if len(self.active_snapshot_paks) > 0: if len(self.active_snapshot_paks) > 0:
self.active_box.set_visible(True) self.active_box.set_visible(True)
else: else:
self.active_box.set_visible(False) self.active_box.set_visible(False)
def generate_leftover_list(self): def generate_leftover_list(self):
for folder in self.leftover_snapshots: for folder in self.leftover_snapshots:
row = LeftoverSnapshotRow(folder, self.long_press_handler) row = LeftoverSnapshotRow(folder, self.long_press_handler)
row.check_button.connect("toggled", lambda *_, _row=row: self.row_select_handler(_row)) row.check_button.connect("toggled", lambda *_, _row=row: self.row_select_handler(_row))
self.leftover_listbox.append(row) self.leftover_listbox.append(row)
if len(self.leftover_snapshots) > 0: if len(self.leftover_snapshots) > 0:
self.leftover_box.set_visible(True) self.leftover_box.set_visible(True)
if len(self.active_snapshot_paks) == 0: if len(self.active_snapshot_paks) == 0:
self.stack.set_visible_child(self.scrolled_window) self.stack.set_visible_child(self.scrolled_window)
else: else:
self.leftover_box.set_visible(False) self.leftover_box.set_visible(False)
def active_select_handler(self, listbox, row, should_show_content=True, refresh=False): def active_select_handler(self, listbox, row, should_show_content=True, refresh=False):
if row.check_button.get_visible(): if row.check_button.get_visible():
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
return return
self.last_activated_row = row self.last_activated_row = row
self.leftover_listbox.select_row(None) self.leftover_listbox.select_row(None)
self.list_page.set_snapshots(row.package, refresh) self.list_page.set_snapshots(row.package, refresh)
self.split_view.set_show_content(should_show_content) self.split_view.set_show_content(should_show_content)
def leftover_select_handler(self, listbox, row, should_show_content=True, refresh=False): def leftover_select_handler(self, listbox, row, should_show_content=True, refresh=False):
if row.check_button.get_visible(): if row.check_button.get_visible():
row.check_button.set_active(not row.check_button.get_active()) row.check_button.set_active(not row.check_button.get_active())
return return
self.last_activated_row = row self.last_activated_row = row
self.active_listbox.select_row(None) self.active_listbox.select_row(None)
self.list_page.set_snapshots(row.folder, refresh) self.list_page.set_snapshots(row.folder, refresh)
self.split_view.set_show_content(should_show_content) self.split_view.set_show_content(should_show_content)
def select_first_row(self): def select_first_row(self):
if row := self.active_listbox.get_row_at_index(0): if row := self.active_listbox.get_row_at_index(0):
self.active_listbox.select_row(row) self.active_listbox.select_row(row)
@@ -183,7 +183,7 @@ class SnapshotPage(Adw.BreakpointBin):
elif row := self.leftover_listbox.get_row_at_index(0): elif row := self.leftover_listbox.get_row_at_index(0):
self.leftover_listbox.select_row(row) self.leftover_listbox.select_row(row)
self.leftover_select_handler(None, row, False, True) self.leftover_select_handler(None, row, False, True)
def show_snapshot(self, package): def show_snapshot(self, package):
i = 0 i = 0
while row := self.active_listbox.get_row_at_index(i): while row := self.active_listbox.get_row_at_index(i):
@@ -198,7 +198,7 @@ class SnapshotPage(Adw.BreakpointBin):
toast = Adw.Toast(title=_("No snapshots for {}").format(package.info['name']), button_label=_("New")) toast = Adw.Toast(title=_("No snapshots for {}").format(package.info['name']), button_label=_("New"))
toast.connect("button-clicked", lambda *_: dialog.present(HostInfo.main_window)) toast.connect("button-clicked", lambda *_: dialog.present(HostInfo.main_window))
self.toast_overlay.add_toast(toast) self.toast_overlay.add_toast(toast)
def start_loading(self): def start_loading(self):
self.last_activated_row = None self.last_activated_row = None
self.search_button.set_active(False) self.search_button.set_active(False)
@@ -211,7 +211,7 @@ class SnapshotPage(Adw.BreakpointBin):
self.leftover_listbox.remove_all() self.leftover_listbox.remove_all()
self.selected_active_rows.clear() self.selected_active_rows.clear()
self.selected_leftover_rows.clear() self.selected_leftover_rows.clear()
def end_loading(self): def end_loading(self):
def callback(*args): def callback(*args):
self.generate_active_list() self.generate_active_list()
@@ -222,16 +222,16 @@ class SnapshotPage(Adw.BreakpointBin):
self.select_first_row() self.select_first_row()
GLib.idle_add(lambda *_: self.stack.set_visible_child(self.scrolled_window)) GLib.idle_add(lambda *_: self.stack.set_visible_child(self.scrolled_window))
GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.split_view)) GLib.idle_add(lambda *_: self.status_stack.set_visible_child(self.split_view))
data_exists = False data_exists = False
for package in HostInfo.flatpaks: for package in HostInfo.flatpaks:
if package.info['id'] == "io.github.flattool.Warehouse": if package.info['id'] == "io.github.flattool.Warehouse":
continue continue
if os.path.exists(package.data_path): if os.path.exists(package.data_path):
data_exists = True data_exists = True
break break
if data_exists: if data_exists:
self.new_button.set_sensitive(True) self.new_button.set_sensitive(True)
self.new_button.set_tooltip_text(None) self.new_button.set_tooltip_text(None)
@@ -242,34 +242,34 @@ class SnapshotPage(Adw.BreakpointBin):
self.new_button.set_tooltip_text(_("No Data Found to Snapshot")) self.new_button.set_tooltip_text(_("No Data Found to Snapshot"))
self.status_new_button.set_sensitive(False) self.status_new_button.set_sensitive(False)
self.status_new_button.set_tooltip_text(_("No Data Found to Snapshot")) self.status_new_button.set_tooltip_text(_("No Data Found to Snapshot"))
Gio.Task.new(None, None, callback).run_in_thread(self.sort_snapshots) Gio.Task.new(None, None, callback).run_in_thread(self.sort_snapshots)
def open_snapshots_folder(self, button): def open_snapshots_folder(self, button):
try: try:
Gio.AppInfo.launch_default_for_uri(f"file://{HostInfo.snapshots_path}", None) Gio.AppInfo.launch_default_for_uri(f"file://{HostInfo.snapshots_path}", None)
self.toast_overlay.add_toast(Adw.Toast.new(_("Opened snapshots folder"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Opened snapshots folder")))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast)
def on_cancel(self): def on_cancel(self):
for worker in self.workers: for worker in self.workers:
worker.do_cancel("manual_cancel") worker.do_cancel("manual_cancel")
if self.new_snapshot_dialog is None: if self.new_snapshot_dialog is None:
return return
for worker in self.new_snapshot_dialog.workers: for worker in self.new_snapshot_dialog.workers:
worker.do_cancel("manual_cancel") worker.do_cancel("manual_cancel")
def on_new(self, *args): def on_new(self, *args):
self.new_snapshot_dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh) self.new_snapshot_dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh)
self.new_snapshot_dialog.present(HostInfo.main_window) self.new_snapshot_dialog.present(HostInfo.main_window)
def refresh(self): def refresh(self):
self.start_loading() self.start_loading()
self.end_loading() self.end_loading()
def on_search(self, search_entry): def on_search(self, search_entry):
text = search_entry.get_text().lower() text = search_entry.get_text().lower()
i = 0 i = 0
@@ -281,7 +281,7 @@ class SnapshotPage(Adw.BreakpointBin):
row.set_visible(True) row.set_visible(True)
total_active_visible += 1 total_active_visible += 1
self.active_box.set_visible(total_active_visible > 0) self.active_box.set_visible(total_active_visible > 0)
i = 0 i = 0
total_leftover_visible = 0 total_leftover_visible = 0
while row := self.leftover_listbox.get_row_at_index(i): while row := self.leftover_listbox.get_row_at_index(i):
@@ -291,18 +291,18 @@ class SnapshotPage(Adw.BreakpointBin):
row.set_visible(True) row.set_visible(True)
total_leftover_visible += 1 total_leftover_visible += 1
self.leftover_box.set_visible(total_leftover_visible > 0) self.leftover_box.set_visible(total_leftover_visible > 0)
if total_active_visible > 0 or total_leftover_visible > 0: if total_active_visible > 0 or total_leftover_visible > 0:
self.stack.set_visible_child(self.scrolled_window) self.stack.set_visible_child(self.scrolled_window)
else: else:
self.stack.set_visible_child(self.no_results) self.stack.set_visible_child(self.no_results)
def sort_func(self, row1, row2): def sort_func(self, row1, row2):
if type(row1) is AppRow: if type(row1) is AppRow:
return row1.package.info['name'].lower() > row2.package.info['name'].lower() return row1.package.info['name'].lower() > row2.package.info['name'].lower()
else: else:
return row1.name.lower() > row2.name.lower() return row1.name.lower() > row2.name.lower()
def set_selection_mode(self, *args): def set_selection_mode(self, *args):
enable = self.select_button.get_active() enable = self.select_button.get_active()
self.active_listbox.set_selection_mode(Gtk.SelectionMode.NONE if enable else Gtk.SelectionMode.SINGLE) self.active_listbox.set_selection_mode(Gtk.SelectionMode.NONE if enable else Gtk.SelectionMode.SINGLE)
@@ -311,32 +311,32 @@ class SnapshotPage(Adw.BreakpointBin):
self.active_listbox.select_row(self.last_activated_row) self.active_listbox.select_row(self.last_activated_row)
elif self.last_activated_row in self.leftover_listbox: elif self.last_activated_row in self.leftover_listbox:
self.leftover_listbox.select_row(self.last_activated_row) self.leftover_listbox.select_row(self.last_activated_row)
i = 0 i = 0
while row := self.active_listbox.get_row_at_index(i): while row := self.active_listbox.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_visible(enable) row.check_button.set_visible(enable)
if not enable: if not enable:
row.check_button.set_active(False) row.check_button.set_active(False)
i = 0 i = 0
while row := self.leftover_listbox.get_row_at_index(i): while row := self.leftover_listbox.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_visible(enable) row.check_button.set_visible(enable)
if not enable: if not enable:
row.check_button.set_active(False) row.check_button.set_active(False)
def select_all_handler(self, *args): def select_all_handler(self, *args):
i = 0 i = 0
while row := self.active_listbox.get_row_at_index(i): while row := self.active_listbox.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_active(True) row.check_button.set_active(True)
i = 0 i = 0
while row := self.leftover_listbox.get_row_at_index(i): while row := self.leftover_listbox.get_row_at_index(i):
i += 1 i += 1
row.check_button.set_active(True) row.check_button.set_active(True)
def row_select_handler(self, row): def row_select_handler(self, row):
if type(row) is AppRow: if type(row) is AppRow:
if row.check_button.get_active(): if row.check_button.get_active():
@@ -348,7 +348,7 @@ class SnapshotPage(Adw.BreakpointBin):
self.selected_leftover_rows.append(row) self.selected_leftover_rows.append(row)
elif row in self.selected_leftover_rows: elif row in self.selected_leftover_rows:
self.selected_leftover_rows.remove(row) self.selected_leftover_rows.remove(row)
total_active = len(self.selected_active_rows) total_active = len(self.selected_active_rows)
total_leftover = len(self.selected_leftover_rows) total_leftover = len(self.selected_leftover_rows)
total = total_active + total_leftover total = total_active + total_leftover
@@ -366,7 +366,7 @@ class SnapshotPage(Adw.BreakpointBin):
row.set_visible(total_active > 0 and total_leftover == 0) row.set_visible(total_active > 0 and total_leftover == 0)
case self.install_from_snapshots: case self.install_from_snapshots:
row.set_visible(total_active == 0 and total_leftover > 0) row.set_visible(total_active == 0 and total_leftover > 0)
def select_copy_handler(self, *args): def select_copy_handler(self, *args):
to_copy = "" to_copy = ""
i = 0 i = 0
@@ -374,48 +374,48 @@ class SnapshotPage(Adw.BreakpointBin):
i += 1 i += 1
if row.check_button.get_active(): if row.check_button.get_active():
to_copy += f"{HostInfo.snapshots_path}{row.package.info['id']}\n" to_copy += f"{HostInfo.snapshots_path}{row.package.info['id']}\n"
i = 0 i = 0
while row := self.leftover_listbox.get_row_at_index(i): while row := self.leftover_listbox.get_row_at_index(i):
i += 1 i += 1
if row.check_button.get_active(): if row.check_button.get_active():
to_copy += f"{HostInfo.snapshots_path}{row.folder}\n" to_copy += f"{HostInfo.snapshots_path}{row.folder}\n"
to_copy = to_copy[0:-1] to_copy = to_copy[0:-1]
HostInfo.clipboard.set(to_copy) HostInfo.clipboard.set(to_copy)
self.toast_overlay.add_toast(Adw.Toast(title=_("Copied Snapshot Paths"))) self.toast_overlay.add_toast(Adw.Toast(title=_("Copied Snapshot Paths")))
def select_new_handler(self): def select_new_handler(self):
packages = [] packages = []
for row in self.selected_active_rows: for row in self.selected_active_rows:
if os.path.exists(row.package.data_path): if os.path.exists(row.package.data_path):
packages.append(row.package) packages.append(row.package)
if len(packages) == 0: if len(packages) == 0:
self.toast_overlay.add_toast(Adw.Toast(title=_("No apps in your selection can be snapshotted"))) self.toast_overlay.add_toast(Adw.Toast(title=_("No apps in your selection can be snapshotted")))
return return
self.new_snapshot_dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh, packages) self.new_snapshot_dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh, packages)
self.new_snapshot_dialog.present(HostInfo.main_window) self.new_snapshot_dialog.present(HostInfo.main_window)
def get_snapshots_from_entry(self, app_ids): def get_snapshots_from_entry(self, app_ids):
id_to_tar = {} id_to_tar = {}
for app_id in app_ids: for app_id in app_ids:
path = f"{HostInfo.snapshots_path}{app_id}" path = f"{HostInfo.snapshots_path}{app_id}"
if not os.path.exists(path): if not os.path.exists(path):
continue continue
tarlist = [] tarlist = []
for file in os.listdir(path): for file in os.listdir(path):
if file.endswith(".tar.zst"): if file.endswith(".tar.zst"):
tarlist.append(file) tarlist.append(file)
id_to_tar[app_id] = tarlist id_to_tar[app_id] = tarlist
if len(tarlist) < 1: if len(tarlist) < 1:
id_to_tar.pop(app_id, None) id_to_tar.pop(app_id, None)
return id_to_tar return id_to_tar
def get_total_fraction(self): def get_total_fraction(self):
total = 0 total = 0
stopped_workers_amount = 0 stopped_workers_amount = 0
@@ -423,29 +423,29 @@ class SnapshotPage(Adw.BreakpointBin):
total += worker.fraction total += worker.fraction
if worker.stop: if worker.stop:
stopped_workers_amount += 1 stopped_workers_amount += 1
if stopped_workers_amount == len(self.workers): if stopped_workers_amount == len(self.workers):
self.snapshotting_status.progress_bar.set_fraction(1) self.snapshotting_status.progress_bar.set_fraction(1)
self.snapshotting_status.progress_label.set_label(f"{len(self.workers)} / {len(self.workers)}") self.snapshotting_status.progress_label.set_label(f"{len(self.workers)} / {len(self.workers)}")
HostInfo.main_window.refresh_handler() HostInfo.main_window.refresh_handler()
self.workers.clear() self.workers.clear()
return False return False
self.snapshotting_status.progress_label.set_label(f"{stopped_workers_amount + 1} / {len(self.workers)}") self.snapshotting_status.progress_label.set_label(f"{stopped_workers_amount + 1} / {len(self.workers)}")
self.snapshotting_status.progress_bar.set_fraction(total / len(self.workers)) self.snapshotting_status.progress_bar.set_fraction(total / len(self.workers))
return True return True
def on_apply_response(self, dialog, response): def on_apply_response(self, dialog, response):
if response != "continue": if response != "continue":
return return
app_ids = [] app_ids = []
for row in self.selected_active_rows: for row in self.selected_active_rows:
app_ids.append(row.package.info['id']) app_ids.append(row.package.info['id'])
for row in self.selected_leftover_rows: for row in self.selected_leftover_rows:
app_ids.append(row.folder) app_ids.append(row.folder)
id_to_tar = self.get_snapshots_from_entry(app_ids) id_to_tar = self.get_snapshots_from_entry(app_ids)
for app_id in id_to_tar: for app_id in id_to_tar:
biggest = 0 biggest = 0
@@ -453,9 +453,9 @@ class SnapshotPage(Adw.BreakpointBin):
epoch = int(tar.split('_')[0]) epoch = int(tar.split('_')[0])
if epoch > biggest: if epoch > biggest:
biggest = epoch biggest = epoch
id_to_tar[app_id] = tar id_to_tar[app_id] = tar
for app_id, tar in id_to_tar.items(): for app_id, tar in id_to_tar.items():
worker = TarWorker( worker = TarWorker(
existing_path=f"{HostInfo.snapshots_path}{app_id}/{tar}", existing_path=f"{HostInfo.snapshots_path}{app_id}/{tar}",
@@ -464,7 +464,7 @@ class SnapshotPage(Adw.BreakpointBin):
) )
self.workers.append(worker) self.workers.append(worker)
worker.extract() worker.extract()
if len(self.workers) > 0: if len(self.workers) > 0:
self.snapshotting_status.title_label.set_label(_("Applying Snapshots")) self.snapshotting_status.title_label.set_label(_("Applying Snapshots"))
self.snapshotting_status.progress_bar.set_fraction(0.0) self.snapshotting_status.progress_bar.set_fraction(0.0)
@@ -473,40 +473,40 @@ class SnapshotPage(Adw.BreakpointBin):
GLib.timeout_add(200, self.get_total_fraction) GLib.timeout_add(200, self.get_total_fraction)
else: else:
self.toast_overlay.add_toast(ErrorToast(_("No snapshots to extract"), _("No snapshots were found to extract"))) self.toast_overlay.add_toast(ErrorToast(_("No snapshots to extract"), _("No snapshots were found to extract")))
def select_apply_handler(self): def select_apply_handler(self):
dialog = Adw.AlertDialog(heading=_("Apply These Snapshots?"), body=_("This will trash the current apps' user data, and apply their newest snapshot")) dialog = Adw.AlertDialog(heading=_("Apply These Snapshots?"), body=_("This will trash the current apps' user data, and apply their newest snapshot"))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
dialog.add_response("continue", _("Continue")) dialog.add_response("continue", _("Continue"))
dialog.connect("response", self.on_apply_response) dialog.connect("response", self.on_apply_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def install_handler(self): def install_handler(self):
package_names = [] package_names = []
for row in self.selected_leftover_rows: for row in self.selected_leftover_rows:
package_names.append(row.folder) package_names.append(row.folder)
AttemptInstallDialog(package_names, lambda is_valid: self.select_button.set_active(not is_valid)) AttemptInstallDialog(package_names, lambda is_valid: self.select_button.set_active(not is_valid))
def selection_trash_handler(self): def selection_trash_handler(self):
if ( if (
len(self.selected_active_rows) + len(self.selected_leftover_rows) < 1 len(self.selected_active_rows) + len(self.selected_leftover_rows) < 1
or self.is_trash_dialog_open or self.is_trash_dialog_open
): ):
return return
def on_response(dialog, response): def on_response(dialog, response):
self.is_trash_dialog_open = False self.is_trash_dialog_open = False
to_trash = [] to_trash = []
if response != "continue": if response != "continue":
return return
for row in self.selected_active_rows: for row in self.selected_active_rows:
to_trash.append(f"{HostInfo.snapshots_path}{row.package.info['id']}") to_trash.append(f"{HostInfo.snapshots_path}{row.package.info['id']}")
for row in self.selected_leftover_rows: for row in self.selected_leftover_rows:
to_trash.append(f"{HostInfo.snapshots_path}{row.folder}") to_trash.append(f"{HostInfo.snapshots_path}{row.folder}")
try: try:
subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True) subprocess.run(['gio', 'trash'] + to_trash, check=True, text=True, capture_output=True)
self.start_loading() self.start_loading()
@@ -514,7 +514,7 @@ class SnapshotPage(Adw.BreakpointBin):
self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed snapshots"))) self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed snapshots")))
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshots"), cpe.stderr).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshots"), cpe.stderr).toast)
self.is_trash_dialog_open = True self.is_trash_dialog_open = True
dialog = Adw.AlertDialog(heading=_("Trash Snapshots?"), body=_("These apps' snapshots will be sent to the trash")) dialog = Adw.AlertDialog(heading=_("Trash Snapshots?"), body=_("These apps' snapshots will be sent to the trash"))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@@ -522,7 +522,7 @@ class SnapshotPage(Adw.BreakpointBin):
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def more_menu_handler(self, listbox, row): def more_menu_handler(self, listbox, row):
self.more_popover.popdown() self.more_popover.popdown()
row = row.get_child() row = row.get_child()
@@ -535,10 +535,10 @@ class SnapshotPage(Adw.BreakpointBin):
self.install_handler() self.install_handler()
case self.trash_snapshots: case self.trash_snapshots:
self.selection_trash_handler() self.selection_trash_handler()
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.__class__.instance = self self.__class__.instance = self
self.main_window = main_window self.main_window = main_window
@@ -552,7 +552,7 @@ class SnapshotPage(Adw.BreakpointBin):
self.new_snapshot_dialog = None self.new_snapshot_dialog = None
self.on_backspace_handler = self.selection_trash_handler self.on_backspace_handler = self.selection_trash_handler
self.on_escape_handler = lambda *_: self.select_button.set_active(False) self.on_escape_handler = lambda *_: self.select_button.set_active(False)
# Apply # Apply
self.search_bar.set_key_capture_widget(HostInfo.main_window) self.search_bar.set_key_capture_widget(HostInfo.main_window)
self.loading_view.set_content(LoadingStatus(_("Loading Snapshots"), _("This should only take a moment"))) self.loading_view.set_content(LoadingStatus(_("Loading Snapshots"), _("This should only take a moment")))
@@ -560,7 +560,7 @@ class SnapshotPage(Adw.BreakpointBin):
self.split_view.set_content(self.list_page) self.split_view.set_content(self.list_page)
self.active_listbox.set_sort_func(self.sort_func) self.active_listbox.set_sort_func(self.sort_func)
self.leftover_listbox.set_sort_func(self.sort_func) self.leftover_listbox.set_sort_func(self.sort_func)
# Connections # Connections
self.active_listbox.connect("row-activated", self.active_select_handler) self.active_listbox.connect("row-activated", self.active_select_handler)
self.leftover_listbox.connect("row-activated", self.leftover_select_handler) self.leftover_listbox.connect("row-activated", self.leftover_select_handler)

View File

@@ -10,38 +10,38 @@ import os
class SnapshotsListPage(Adw.NavigationPage): class SnapshotsListPage(Adw.NavigationPage):
__gtype_name__ = "SnapshotsListPage" __gtype_name__ = "SnapshotsListPage"
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
toolbar_view = gtc() toolbar_view = gtc()
listbox = gtc() listbox = gtc()
toast_overlay = gtc() toast_overlay = gtc()
open_button = gtc() open_button = gtc()
new_button = gtc() new_button = gtc()
def thread(self, *args): def thread(self, *args):
is_leftover = type(self.package_or_folder) is str is_leftover = type(self.package_or_folder) is str
for snapshot in os.listdir(folder := f"{self.snapshots_path}{self.current_folder}/"): for snapshot in os.listdir(folder := f"{self.snapshots_path}{self.current_folder}/"):
if snapshot.endswith(".json"): if snapshot.endswith(".json"):
continue continue
row = SnapshotBox(self, snapshot, folder, self.toast_overlay) row = SnapshotBox(self, snapshot, folder, self.toast_overlay)
row.apply_button.set_sensitive(not is_leftover) row.apply_button.set_sensitive(not is_leftover)
self.snapshots_rows.append(row) self.snapshots_rows.append(row)
if is_leftover: if is_leftover:
row.apply_button.set_tooltip_text(_("App not Installed")) row.apply_button.set_tooltip_text(_("App not Installed"))
def callback(self, *args): def callback(self, *args):
if len(self.snapshots_rows) == 0: if len(self.snapshots_rows) == 0:
self.parent_page.refresh() self.parent_page.refresh()
return return
for i, row in enumerate(self.snapshots_rows): for i, row in enumerate(self.snapshots_rows):
self.listbox.append(row) self.listbox.append(row)
self.listbox.get_row_at_index(i).set_activatable(False) self.listbox.get_row_at_index(i).set_activatable(False)
def set_snapshots(self, package_or_folder, refresh=False): def set_snapshots(self, package_or_folder, refresh=False):
if package_or_folder == self.package_or_folder and not refresh: if package_or_folder == self.package_or_folder and not refresh:
return return
folder = None folder = None
self.package_or_folder = package_or_folder self.package_or_folder = package_or_folder
if type(package_or_folder) is str: if type(package_or_folder) is str:
@@ -58,53 +58,53 @@ class SnapshotsListPage(Adw.NavigationPage):
else: else:
self.new_button.set_sensitive(False) self.new_button.set_sensitive(False)
self.new_button.set_tooltip_text(_("No Data Found to Snapshot")) self.new_button.set_tooltip_text(_("No Data Found to Snapshot"))
self.current_folder = folder self.current_folder = folder
self.snapshots_rows.clear() self.snapshots_rows.clear()
self.listbox.remove_all() self.listbox.remove_all()
Gio.Task.new(None, None, self.callback).run_in_thread(self.thread) Gio.Task.new(None, None, self.callback).run_in_thread(self.thread)
def open_snapshots_folder(self, button): def open_snapshots_folder(self, button):
path = f"{self.snapshots_path}{self.current_folder}/" path = f"{self.snapshots_path}{self.current_folder}/"
try: try:
if not os.path.exists(path): if not os.path.exists(path):
raise Exception(f"error: File '{path}' does not exist") raise Exception(f"error: File '{path}' does not exist")
Gio.AppInfo.launch_default_for_uri(f"file://{path}", None) Gio.AppInfo.launch_default_for_uri(f"file://{path}", None)
self.toast_overlay.add_toast(Adw.Toast.new(_("Opened snapshots folder"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Opened snapshots folder")))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast)
def on_done(self): def on_done(self):
self.parent_page.status_stack.set_visible_child(self.parent_page.split_view) self.parent_page.status_stack.set_visible_child(self.parent_page.split_view)
self.set_snapshots(self.package_or_folder, refresh=True) self.set_snapshots(self.package_or_folder, refresh=True)
def on_new(self, button): def on_new(self, button):
self.parent_page.new_snapshot_dialog = NewSnapshotDialog(self.parent_page, self.parent_page.snapshotting_status, self.on_done, [self.package_or_folder]) self.parent_page.new_snapshot_dialog = NewSnapshotDialog(self.parent_page, self.parent_page.snapshotting_status, self.on_done, [self.package_or_folder])
self.parent_page.new_snapshot_dialog.present(HostInfo.main_window) self.parent_page.new_snapshot_dialog.present(HostInfo.main_window)
def sort_func(self, row1, row2): def sort_func(self, row1, row2):
row1 = row1.get_child() row1 = row1.get_child()
row2 = row2.get_child() row2 = row2.get_child()
return row1.epoch > row2.epoch return row1.epoch > row2.epoch
def on_trash(self): def on_trash(self):
self.set_snapshots(self.package_or_folder, refresh=True) self.set_snapshots(self.package_or_folder, refresh=True)
def __init__(self, parent_page, **kwargs): def __init__(self, parent_page, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.parent_page = parent_page self.parent_page = parent_page
self.snapshots_path = HostInfo.snapshots_path self.snapshots_path = HostInfo.snapshots_path
self.current_folder = None self.current_folder = None
self.package_or_folder = None self.package_or_folder = None
self.snapshots_rows = [] self.snapshots_rows = []
# Connections # Connections
self.open_button.connect("clicked", self.open_snapshots_folder) self.open_button.connect("clicked", self.open_snapshots_folder)
self.new_button.connect("clicked", self.on_new) self.new_button.connect("clicked", self.on_new)
# Apply # Apply
self.listbox.set_sort_func(self.sort_func) self.listbox.set_sort_func(self.sort_func)

View File

@@ -8,102 +8,102 @@ class TarWorker:
try: try:
if not os.path.exists(self.new_path): if not os.path.exists(self.new_path):
os.makedirs(self.new_path) os.makedirs(self.new_path)
self.total = int(subprocess.run(['du', '-s', self.existing_path], check=True, text=True, capture_output=True).stdout.split('\t')[0]) self.total = int(subprocess.run(['du', '-s', self.existing_path], check=True, text=True, capture_output=True).stdout.split('\t')[0])
self.total /= 2.2 # estimate for space savings self.total /= 2.2 # estimate for space savings
self.process = subprocess.Popen(['tar', 'cafv', f'{self.new_path}/{self.file_name}.tar.zst', '-C', self.existing_path, '.'], self.process = subprocess.Popen(['tar', 'cafv', f'{self.new_path}/{self.file_name}.tar.zst', '-C', self.existing_path, '.'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE stderr=subprocess.PIPE
) )
stdout, stderr = self.process.communicate() stdout, stderr = self.process.communicate()
if self.process.returncode != 0: if self.process.returncode != 0:
raise subprocess.CalledProcessError(self.process.returncode, self.process.args, output=stdout, stderr=stderr) raise subprocess.CalledProcessError(self.process.returncode, self.process.args, output=stdout, stderr=stderr)
with open(f"{self.new_path}/{self.file_name}.json", 'w') as file: with open(f"{self.new_path}/{self.file_name}.json", 'w') as file:
data = { data = {
'snapshot_version': 1, 'snapshot_version': 1,
'name': self.name, 'name': self.name,
} }
json.dump(data, file, indent=4) json.dump(data, file, indent=4)
self.stop = True # tell the check timeout to stop, because we know the file is done being made self.stop = True # tell the check timeout to stop, because we know the file is done being made
HostInfo.main_window.remove_refresh_lockout("managing snapshot") HostInfo.main_window.remove_refresh_lockout("managing snapshot")
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.do_cancel(cpe.stderr.decode()) # stderr is in bytes, so decode it self.do_cancel(cpe.stderr.decode()) # stderr is in bytes, so decode it
except Exception as e: except Exception as e:
self.do_cancel(str(e)) self.do_cancel(str(e))
def extract_thread(self, *args): def extract_thread(self, *args):
try: try:
if os.path.exists(self.new_path): if os.path.exists(self.new_path):
subprocess.run(['gio', 'trash', self.new_path], capture_output=True, check=True) # trash the current user data, because new data will go in its place subprocess.run(['gio', 'trash', self.new_path], capture_output=True, check=True) # trash the current user data, because new data will go in its place
os.makedirs(self.new_path) # create the new user data path os.makedirs(self.new_path) # create the new user data path
self.total = int(subprocess.run(['du', '-s', self.existing_path], check=True, text=True, capture_output=True).stdout.split('\t')[0]) self.total = int(subprocess.run(['du', '-s', self.existing_path], check=True, text=True, capture_output=True).stdout.split('\t')[0])
self.total *= 2.2 # estimate from space savings self.total *= 2.2 # estimate from space savings
self.process = subprocess.Popen(['tar', '--zstd', '-xvf', self.existing_path, '-C', self.new_path], self.process = subprocess.Popen(['tar', '--zstd', '-xvf', self.existing_path, '-C', self.new_path],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE stderr=subprocess.PIPE
) )
stdout, stderr = self.process.communicate() stdout, stderr = self.process.communicate()
if self.process.returncode != 0: if self.process.returncode != 0:
raise subprocess.CalledProcessError(self.process.returncode, self.process.args, output=stdout, stderr=stderr) raise subprocess.CalledProcessError(self.process.returncode, self.process.args, output=stdout, stderr=stderr)
self.stop = True # tell the check timeout to stop, because we know the file is done being made self.stop = True # tell the check timeout to stop, because we know the file is done being made
HostInfo.main_window.remove_refresh_lockout("managing snapshot") HostInfo.main_window.remove_refresh_lockout("managing snapshot")
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.do_cancel(cpe.stderr.decode()) self.do_cancel(cpe.stderr.decode())
except Exception as e: except Exception as e:
self.do_cancel(str(e)) self.do_cancel(str(e))
def do_cancel(self, error_str): def do_cancel(self, error_str):
if self.has_cancelled or self.stop: if self.has_cancelled or self.stop:
return return
self.has_cancelled = True self.has_cancelled = True
self.process.terminate() self.process.terminate()
self.process.wait() self.process.wait()
if len(self.files_to_trash_on_cancel) > 0: if len(self.files_to_trash_on_cancel) > 0:
try: try:
subprocess.run(['gio', 'trash'] + self.files_to_trash_on_cancel, capture_output=True, check=True) subprocess.run(['gio', 'trash'] + self.files_to_trash_on_cancel, capture_output=True, check=True)
except Exception: except Exception:
pass pass
self.stop = True self.stop = True
HostInfo.main_window.remove_refresh_lockout("managing snapshot") HostInfo.main_window.remove_refresh_lockout("managing snapshot")
if self.toast_overlay and error_str != "manual_cancel": if self.toast_overlay and error_str != "manual_cancel":
self.toast_overlay.add_toast(ErrorToast(_("Error in snapshot handling"), error_str).toast) self.toast_overlay.add_toast(ErrorToast(_("Error in snapshot handling"), error_str).toast)
def check_size(self, check_path): def check_size(self, check_path):
try: try:
output = subprocess.run(['du', '-s', check_path], check=True, text=True, capture_output=True).stdout.split('\t')[0] output = subprocess.run(['du', '-s', check_path], check=True, text=True, capture_output=True).stdout.split('\t')[0]
working_total = float(output) working_total = float(output)
self.fraction = working_total / self.total self.fraction = working_total / self.total
return not self.stop return not self.stop
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
return not self.stop # continue the timeout or stop the timeout return not self.stop # continue the timeout or stop the timeout
def compress(self): def compress(self):
self.stop = False self.stop = False
self.files_to_trash_on_cancel = [f'{self.new_path}/{self.file_name}.tar.zst', f'{self.new_path}/{self.file_name}.json'] self.files_to_trash_on_cancel = [f'{self.new_path}/{self.file_name}.tar.zst', f'{self.new_path}/{self.file_name}.json']
HostInfo.main_window.add_refresh_lockout("managing snapshot") HostInfo.main_window.add_refresh_lockout("managing snapshot")
Gio.Task.new(None, None, None).run_in_thread(self.compress_thread) Gio.Task.new(None, None, None).run_in_thread(self.compress_thread)
GLib.timeout_add(200, self.check_size, f"{self.new_path}/{self.file_name}.tar.zst") GLib.timeout_add(200, self.check_size, f"{self.new_path}/{self.file_name}.tar.zst")
def extract(self): def extract(self):
self.stop = False self.stop = False
self.files_to_trash_on_cancel = [self.new_path] self.files_to_trash_on_cancel = [self.new_path]
HostInfo.main_window.add_refresh_lockout("managing snapshot") HostInfo.main_window.add_refresh_lockout("managing snapshot")
Gio.Task.new(None, None, None).run_in_thread(self.extract_thread) Gio.Task.new(None, None, None).run_in_thread(self.extract_thread)
GLib.timeout_add(200, self.check_size, self.new_path) GLib.timeout_add(200, self.check_size, self.new_path)
def __init__(self, existing_path, new_path, file_name="", name="", toast_overlay=None): def __init__(self, existing_path, new_path, file_name="", name="", toast_overlay=None):
self.existing_path = existing_path self.existing_path = existing_path
self.new_path = new_path self.new_path = new_path

View File

@@ -8,20 +8,20 @@ import subprocess
class DataBox(Gtk.ListBox): class DataBox(Gtk.ListBox):
__gtype_name__ = 'DataBox' __gtype_name__ = 'DataBox'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
row = gtc() row = gtc()
image = gtc() image = gtc()
title_label = gtc() title_label = gtc()
subtitle_label = gtc() subtitle_label = gtc()
spinner = gtc() spinner = gtc()
size_label = gtc() size_label = gtc()
copy_button = gtc() copy_button = gtc()
open_button = gtc() open_button = gtc()
install_button = gtc() install_button = gtc()
trash_button = gtc() trash_button = gtc()
check_button = gtc() check_button = gtc()
def human_readable_size(self): def human_readable_size(self):
working_size = self.size working_size = self.size
units = ['KB', 'MB', 'GB', 'TB'] units = ['KB', 'MB', 'GB', 'TB']
@@ -31,19 +31,19 @@ class DataBox(Gtk.ListBox):
return f"~ {round(working_size)} {unit}" return f"~ {round(working_size)} {unit}"
working_size /= 1024 working_size /= 1024
return f"~ {round(working_size)} PB" return f"~ {round(working_size)} PB"
def get_size(self, *args): def get_size(self, *args):
self.size = int(subprocess.run(['du', '-s', self.data_path], capture_output=True, text=True).stdout.split("\t")[0]) self.size = int(subprocess.run(['du', '-s', self.data_path], capture_output=True, text=True).stdout.split("\t")[0])
def show_size(self): def show_size(self):
def callback(*args): def callback(*args):
self.size_label.set_label(self.human_readable_size()) self.size_label.set_label(self.human_readable_size())
self.spinner.set_visible(False) self.spinner.set_visible(False)
if self.callback: if self.callback:
self.callback(self.size) self.callback(self.size)
Gio.Task.new(None, None, callback).run_in_thread(self.get_size) Gio.Task.new(None, None, callback).run_in_thread(self.get_size)
def idle_stuff(self): def idle_stuff(self):
self.title_label.set_label(self.title) self.title_label.set_label(self.title)
self.subtitle_label.set_label(self.subtitle) self.subtitle_label.set_label(self.subtitle)
@@ -51,31 +51,31 @@ class DataBox(Gtk.ListBox):
if self.icon_path: if self.icon_path:
self.image.add_css_class("icon-dropshadow") self.image.add_css_class("icon-dropshadow")
self.image.set_from_file(self.icon_path) self.image.set_from_file(self.icon_path)
def copy_handler(self, *args): def copy_handler(self, *args):
try: try:
HostInfo.clipboard.set(self.data_path) HostInfo.clipboard.set(self.data_path)
self.toast_overlay.add_toast(Adw.Toast.new(_("Copied data path"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Copied data path")))
except Exception as e: except Exception as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not copy data path"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not copy data path"), str(e)).toast)
def open_handler(self, *args): def open_handler(self, *args):
try: try:
Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None) Gio.AppInfo.launch_default_for_uri(f"file://{self.data_path}", None)
self.toast_overlay.add_toast(Adw.Toast.new(_("Opened data folder"))) self.toast_overlay.add_toast(Adw.Toast.new(_("Opened data folder")))
except GLib.GError as e: except GLib.GError as e:
self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not open folder"), str(e)).toast)
def install_handler(self, *args): def install_handler(self, *args):
self.parent_page.should_rclick = False self.parent_page.should_rclick = False
def why_cant_this_just_be_a_lambda(*args): def why_cant_this_just_be_a_lambda(*args):
self.parent_page.should_rclick = True self.parent_page.should_rclick = True
AttemptInstallDialog([self.subtitle], why_cant_this_just_be_a_lambda) AttemptInstallDialog([self.subtitle], why_cant_this_just_be_a_lambda)
def trash_handler(self, *args): def trash_handler(self, *args):
self.failed_trash = False self.failed_trash = False
def thread(*args): def thread(*args):
try: try:
subprocess.run(['gio', 'trash', self.data_path], check=True, text=True, capture_output=True) subprocess.run(['gio', 'trash', self.data_path], check=True, text=True, capture_output=True)
@@ -83,17 +83,17 @@ class DataBox(Gtk.ListBox):
properties_package = properties_page.package properties_package = properties_page.package
if not properties_package is None: if not properties_package is None:
properties_page.set_properties(properties_package, True) properties_page.set_properties(properties_package, True)
snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page
snapshot_list_package = snapshot_list_page.package_or_folder snapshot_list_package = snapshot_list_page.package_or_folder
if not snapshot_list_package is None: if not snapshot_list_package is None:
snapshot_list_page.set_snapshots(snapshot_list_package, True) snapshot_list_page.set_snapshots(snapshot_list_package, True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
self.failed_trash = cpe.stderr self.failed_trash = cpe.stderr
except Exception as e: except Exception as e:
self.failed_trash = e self.failed_trash = e
def callback(*args): def callback(*args):
if self.failed_trash: if self.failed_trash:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(self.failed_trash)).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(self.failed_trash)).toast)
@@ -101,14 +101,14 @@ class DataBox(Gtk.ListBox):
self.toast_overlay.add_toast(Adw.Toast.new("Trashed data")) self.toast_overlay.add_toast(Adw.Toast.new("Trashed data"))
if self.trash_callback: if self.trash_callback:
self.trash_callback(self) self.trash_callback(self)
def on_response(_, response): def on_response(_, response):
self.parent_page.should_rclick = True self.parent_page.should_rclick = True
if response != "continue": if response != "continue":
return return
Gio.Task.new(None, None, callback).run_in_thread(thread) Gio.Task.new(None, None, callback).run_in_thread(thread)
self.parent_page.should_rclick = False self.parent_page.should_rclick = False
dialog = Adw.AlertDialog(heading=_("Trash {}'s Data?").format(self.title), body=_("{}'s data will be sent to the trash").format(self.title)) dialog = Adw.AlertDialog(heading=_("Trash {}'s Data?").format(self.title), body=_("{}'s data will be sent to the trash").format(self.title))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@@ -116,10 +116,10 @@ class DataBox(Gtk.ListBox):
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(HostInfo.main_window) dialog.present(HostInfo.main_window)
def __init__(self, parent_page, toast_overlay, is_leftover, title, subtitle, data_path, icon_path=None, callback=None, trash_callback=None, **kwargs): def __init__(self, parent_page, toast_overlay, is_leftover, title, subtitle, data_path, icon_path=None, callback=None, trash_callback=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.parent_page = parent_page self.parent_page = parent_page
self.toast_overlay = toast_overlay self.toast_overlay = toast_overlay
@@ -132,7 +132,7 @@ class DataBox(Gtk.ListBox):
self.trash_callback = trash_callback self.trash_callback = trash_callback
self.size = None self.size = None
self.failed_trash = None self.failed_trash = None
# Apply # Apply
self.idle_stuff() self.idle_stuff()
self.show_size() self.show_size()
@@ -140,7 +140,7 @@ class DataBox(Gtk.ListBox):
self.check_button.set_active = lambda *_: None self.check_button.set_active = lambda *_: None
self.check_button.set_sensitive(False) self.check_button.set_sensitive(False)
self.trash_button.set_sensitive(False) self.trash_button.set_sensitive(False)
# Connections # Connections
self.copy_button.connect("clicked", self.copy_handler) self.copy_button.connect("clicked", self.copy_handler)
self.open_button.connect("clicked", self.open_handler) self.open_button.connect("clicked", self.open_handler)

View File

@@ -9,7 +9,7 @@ class DataSubpage(Gtk.Stack):
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
scrolled_window = gtc() scrolled_window = gtc()
label_box = gtc() label_box = gtc()
subtitle_size_box = gtc() subtitle_size_box = gtc()
title = gtc() title = gtc()
@@ -100,7 +100,7 @@ class DataSubpage(Gtk.Stack):
self.selected_boxes.remove(box) self.selected_boxes.remove(box)
except ValueError: except ValueError:
pass pass
total = len(self.selected_boxes) total = len(self.selected_boxes)
self.subtitle.set_visible(not total == 0) self.subtitle.set_visible(not total == 0)
self.size_label.set_visible(total == 0) self.size_label.set_visible(total == 0)
@@ -148,7 +148,7 @@ class DataSubpage(Gtk.Stack):
box = DataBox(self, self.parent_page.toast_overlay, True, folder.split('.')[-1], folder, f"{HostInfo.home}/.var/app/{folder}", None, self.box_size_callback, self.trash_handler) box = DataBox(self, self.parent_page.toast_overlay, True, folder.split('.')[-1], folder, f"{HostInfo.home}/.var/app/{folder}", None, self.box_size_callback, self.trash_handler)
box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box)) box.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box))
self.flow_box.append(box) self.flow_box.append(box)
idx = 0 idx = 0
while box := self.flow_box.get_child_at_index(idx): while box := self.flow_box.get_child_at_index(idx):
idx += 1 idx += 1

View File

@@ -11,7 +11,7 @@ import os, subprocess
class UserDataPage(Adw.BreakpointBin): class UserDataPage(Adw.BreakpointBin):
__gtype_name__ = 'UserDataPage' __gtype_name__ = 'UserDataPage'
gtc = Gtk.Template.Child gtc = Gtk.Template.Child
bpt = gtc() bpt = gtc()
status_stack = gtc() status_stack = gtc()
loading_view = gtc() loading_view = gtc()
@@ -26,13 +26,13 @@ class UserDataPage(Adw.BreakpointBin):
toast_overlay = gtc() toast_overlay = gtc()
stack = gtc() stack = gtc()
revealer = gtc() revealer = gtc()
sort_ascend = gtc() sort_ascend = gtc()
sort_descend = gtc() sort_descend = gtc()
sort_name = gtc() sort_name = gtc()
sort_id = gtc() sort_id = gtc()
sort_size = gtc() sort_size = gtc()
select_all_button = gtc() select_all_button = gtc()
copy_button = gtc() copy_button = gtc()
trash_button = gtc() trash_button = gtc()
@@ -42,7 +42,7 @@ class UserDataPage(Adw.BreakpointBin):
more_menu = gtc() more_menu = gtc()
more_trash = gtc() more_trash = gtc()
more_install = gtc() more_install = gtc()
# Referred to in the main window # Referred to in the main window
# It is used to determine if a new page should be made or not # It is used to determine if a new page should be made or not
# This must be set to the created object from within the class's __init__ method # This must be set to the created object from within the class's __init__ method
@@ -51,23 +51,23 @@ class UserDataPage(Adw.BreakpointBin):
data_path = f"{HostInfo.home}/.var/app" data_path = f"{HostInfo.home}/.var/app"
bpt_is_applied = False bpt_is_applied = False
is_trash_dialog_open = False is_trash_dialog_open = False
def sort_data(self, *args): def sort_data(self, *args):
self.data_flatpaks.clear() self.data_flatpaks.clear()
self.active_data.clear() self.active_data.clear()
self.leftover_data.clear() self.leftover_data.clear()
# paks = dict(HostInfo.id_to_flatpak) # paks = dict(HostInfo.id_to_flatpak)
if not os.path.exists(self.data_path): if not os.path.exists(self.data_path):
return return
for folder in os.listdir(self.data_path): for folder in os.listdir(self.data_path):
try: try:
self.data_flatpaks.append(HostInfo.id_to_flatpak[folder]) self.data_flatpaks.append(HostInfo.id_to_flatpak[folder])
self.active_data.append(folder) self.active_data.append(folder)
except KeyError: except KeyError:
self.leftover_data.append(folder) self.leftover_data.append(folder)
def start_loading(self, *args): def start_loading(self, *args):
self.status_stack.set_visible_child(self.loading_view) self.status_stack.set_visible_child(self.loading_view)
self.search_button.set_active(False) self.search_button.set_active(False)
@@ -76,23 +76,23 @@ class UserDataPage(Adw.BreakpointBin):
self.adp.spinner.set_visible(True) self.adp.spinner.set_visible(True)
self.ldp.size_label.set_label(_("Loading Size")) self.ldp.size_label.set_label(_("Loading Size"))
self.ldp.spinner.set_visible(True) self.ldp.spinner.set_visible(True)
def end_loading(self, *args): def end_loading(self, *args):
def callback(*args): def callback(*args):
self.adp.generate_list(self.data_flatpaks, self.active_data) self.adp.generate_list(self.data_flatpaks, self.active_data)
self.ldp.generate_list([], self.leftover_data) self.ldp.generate_list([], self.leftover_data)
Gio.Task.new(None, None, callback).run_in_thread(self.sort_data) Gio.Task.new(None, None, callback).run_in_thread(self.sort_data)
def sort_button_handler(self, button): def sort_button_handler(self, button):
if button in {self.sort_ascend, self.sort_descend}: if button in {self.sort_ascend, self.sort_descend}:
self.settings.set_boolean("sort-ascend", self.sort_ascend.get_active()) self.settings.set_boolean("sort-ascend", self.sort_ascend.get_active())
else: else:
self.settings.set_string("sort-mode", self.buttons_to_sort_modes[button]) self.settings.set_string("sort-mode", self.buttons_to_sort_modes[button])
self.adp.update_sort_mode() self.adp.update_sort_mode()
self.ldp.update_sort_mode() self.ldp.update_sort_mode()
def load_sort_settings(self): def load_sort_settings(self):
mode = self.settings.get_string("sort-mode") mode = self.settings.get_string("sort-mode")
ascend = self.settings.get_boolean("sort-ascend") ascend = self.settings.get_boolean("sort-ascend")
@@ -100,7 +100,7 @@ class UserDataPage(Adw.BreakpointBin):
(self.sort_ascend if ascend else self.sort_descend).set_active(True) (self.sort_ascend if ascend else self.sort_descend).set_active(True)
self.adp.update_sort_mode() self.adp.update_sort_mode()
self.ldp.update_sort_mode() self.ldp.update_sort_mode()
def view_change_handler(self, *args): def view_change_handler(self, *args):
child = self.stack.get_visible_child() child = self.stack.get_visible_child()
if child.total_size == 0: if child.total_size == 0:
@@ -116,17 +116,17 @@ class UserDataPage(Adw.BreakpointBin):
self.select_button.set_sensitive(True) self.select_button.set_sensitive(True)
self.sort_button.set_sensitive(True) self.sort_button.set_sensitive(True)
self.search_entry.set_editable(True) self.search_entry.set_editable(True)
self.more_button.set_visible(child is self.ldp and self.bpt_is_applied) self.more_button.set_visible(child is self.ldp and self.bpt_is_applied)
self.install_button.set_visible(child is self.ldp and not self.bpt_is_applied) self.install_button.set_visible(child is self.ldp and not self.bpt_is_applied)
self.trash_button.set_visible(child is self.adp or not self.bpt_is_applied) self.trash_button.set_visible(child is self.adp or not self.bpt_is_applied)
has_selected = len(child.selected_boxes) > 0 has_selected = len(child.selected_boxes) > 0
self.copy_button.set_sensitive(has_selected) self.copy_button.set_sensitive(has_selected)
self.trash_button.set_sensitive(has_selected) self.trash_button.set_sensitive(has_selected)
self.install_button.set_sensitive(has_selected) self.install_button.set_sensitive(has_selected)
self.more_button.set_sensitive(has_selected) self.more_button.set_sensitive(has_selected)
def select_toggle_handler(self, *args): def select_toggle_handler(self, *args):
active = self.select_button.get_active() active = self.select_button.get_active()
self.adp.set_selection_mode(active) self.adp.set_selection_mode(active)
@@ -136,27 +136,27 @@ class UserDataPage(Adw.BreakpointBin):
self.trash_button.set_sensitive(False) self.trash_button.set_sensitive(False)
self.install_button.set_sensitive(False) self.install_button.set_sensitive(False)
self.more_button.set_sensitive(False) self.more_button.set_sensitive(False)
def select_all_handler(self, *args): def select_all_handler(self, *args):
child = self.stack.get_visible_child() child = self.stack.get_visible_child()
child.select_all_handler() child.select_all_handler()
def copy_handler(self, *args): def copy_handler(self, *args):
child = self.stack.get_visible_child() child = self.stack.get_visible_child()
to_copy = "" to_copy = ""
for box in child.selected_boxes: for box in child.selected_boxes:
to_copy += "\n" + box.data_path to_copy += "\n" + box.data_path
if len(to_copy) == 0: if len(to_copy) == 0:
self.toast_overlay.add_toast(ErrorToast(_("Could not copy paths"), _("No boxes were selected")).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not copy paths"), _("No boxes were selected")).toast)
else: else:
HostInfo.clipboard.set(to_copy.replace("\n", "", 1)) HostInfo.clipboard.set(to_copy.replace("\n", "", 1))
self.toast_overlay.add_toast(Adw.Toast(title=_("Copied paths"))) self.toast_overlay.add_toast(Adw.Toast(title=_("Copied paths")))
def selection_trash_handler(self, *args): def selection_trash_handler(self, *args):
error = [None] error = [None]
child = self.stack.get_visible_child() child = self.stack.get_visible_child()
def thread(path): def thread(path):
cmd = ['gio', 'trash'] + path cmd = ['gio', 'trash'] + path
try: try:
@@ -165,17 +165,17 @@ class UserDataPage(Adw.BreakpointBin):
properties_package = properties_page.package properties_package = properties_page.package
if not properties_package is None: if not properties_package is None:
properties_page.set_properties(properties_package, True) properties_page.set_properties(properties_package, True)
snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page snapshot_list_page = HostInfo.main_window.pages[HostInfo.main_window.snapshots_row].list_page
snapshot_list_package = snapshot_list_page.package_or_folder snapshot_list_package = snapshot_list_page.package_or_folder
if not snapshot_list_package is None: if not snapshot_list_package is None:
snapshot_list_page.set_snapshots(snapshot_list_package, True) snapshot_list_page.set_snapshots(snapshot_list_package, True)
except subprocess.CalledProcessError as cpe: except subprocess.CalledProcessError as cpe:
error[0] = cpe.stderr error[0] = cpe.stderr
except Exception as e: except Exception as e:
error[0] = e error[0] = e
def callback(*args): def callback(*args):
self.start_loading() self.start_loading()
self.end_loading() self.end_loading()
@@ -183,27 +183,27 @@ class UserDataPage(Adw.BreakpointBin):
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(error[0])).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), str(error[0])).toast)
else: else:
self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed data"))) self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed data")))
def on_response(dialog, response): def on_response(dialog, response):
self.is_trash_dialog_open = False self.is_trash_dialog_open = False
if response != "continue": if response != "continue":
return return
to_trash = [] to_trash = []
for box in child.selected_boxes: for box in child.selected_boxes:
to_trash.append(box.data_path) to_trash.append(box.data_path)
if len(to_trash) == 0: if len(to_trash) == 0:
self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), _("No boxes were selected")).toast) self.toast_overlay.add_toast(ErrorToast(_("Could not trash data"), _("No boxes were selected")).toast)
return return
self.select_button.set_active(False) self.select_button.set_active(False)
self.status_stack.set_visible_child(self.loading_view) self.status_stack.set_visible_child(self.loading_view)
Gio.Task.new(None, None, callback).run_in_thread(lambda *_: thread(to_trash)) Gio.Task.new(None, None, callback).run_in_thread(lambda *_: thread(to_trash))
if len(child.selected_boxes) < 1 or self.is_trash_dialog_open: if len(child.selected_boxes) < 1 or self.is_trash_dialog_open:
return return
self.is_trash_dialog_open = True self.is_trash_dialog_open = True
dialog = Adw.AlertDialog(heading=_("Trash Data?"), body=_("Data will be sent to the trash")) dialog = Adw.AlertDialog(heading=_("Trash Data?"), body=_("Data will be sent to the trash"))
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@@ -211,7 +211,7 @@ class UserDataPage(Adw.BreakpointBin):
dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(ErrorToast.main_window) dialog.present(ErrorToast.main_window)
def breakpoint_handler(self, bpt, is_applied): def breakpoint_handler(self, bpt, is_applied):
self.bpt_is_applied = is_applied self.bpt_is_applied = is_applied
self.adp.label_box.set_orientation(Gtk.Orientation.VERTICAL if is_applied else Gtk.Orientation.HORIZONTAL) self.adp.label_box.set_orientation(Gtk.Orientation.VERTICAL if is_applied else Gtk.Orientation.HORIZONTAL)
@@ -220,15 +220,15 @@ class UserDataPage(Adw.BreakpointBin):
self.install_button.set_visible(child is self.ldp and not is_applied) self.install_button.set_visible(child is self.ldp and not is_applied)
self.more_button.set_visible(child is self.ldp and is_applied) self.more_button.set_visible(child is self.ldp and is_applied)
self.trash_button.set_visible(child is self.adp or not is_applied) self.trash_button.set_visible(child is self.adp or not is_applied)
def install_handler(self, *args): def install_handler(self, *args):
child = self.stack.get_visible_child() child = self.stack.get_visible_child()
package_names = [] package_names = []
for box in child.selected_boxes: for box in child.selected_boxes:
package_names.append(box.subtitle) package_names.append(box.subtitle)
AttemptInstallDialog(package_names, lambda is_valid: self.select_button.set_active(not is_valid)) AttemptInstallDialog(package_names, lambda is_valid: self.select_button.set_active(not is_valid))
def more_menu_handler(self, listbox, row): def more_menu_handler(self, listbox, row):
self.more_popover.popdown() self.more_popover.popdown()
row = row.get_child() row = row.get_child()
@@ -237,10 +237,10 @@ class UserDataPage(Adw.BreakpointBin):
self.install_handler() self.install_handler()
case self.more_trash: case self.more_trash:
self.selection_trash_handler() self.selection_trash_handler()
def __init__(self, main_window, **kwargs): def __init__(self, main_window, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# Extra Object Creation # Extra Object Creation
self.__class__.instance = self self.__class__.instance = self
self.adp = DataSubpage(_("Active Data"), self, True, main_window) self.adp = DataSubpage(_("Active Data"), self, True, main_window)
@@ -258,11 +258,11 @@ class UserDataPage(Adw.BreakpointBin):
self.buttons_to_sort_modes = {} self.buttons_to_sort_modes = {}
self.on_backspace_handler = self.selection_trash_handler self.on_backspace_handler = self.selection_trash_handler
self.on_escape_handler = lambda *_: self.select_button.set_active(False) self.on_escape_handler = lambda *_: self.select_button.set_active(False)
# Apply # Apply
for key, button in self.sort_modes_to_buttons.items(): for key, button in self.sort_modes_to_buttons.items():
self.buttons_to_sort_modes[button] = key self.buttons_to_sort_modes[button] = key
self.stack.add_titled_with_icon( self.stack.add_titled_with_icon(
child=self.adp, child=self.adp,
name="active", name="active",
@@ -275,7 +275,7 @@ class UserDataPage(Adw.BreakpointBin):
title=_("Leftover Data"), title=_("Leftover Data"),
icon_name="folder-templates-symbolic", icon_name="folder-templates-symbolic",
) )
# Connections # Connections
self.stack.connect("notify::visible-child", self.view_change_handler) self.stack.connect("notify::visible-child", self.view_change_handler)
self.select_button.connect("toggled", self.select_toggle_handler) self.select_button.connect("toggled", self.select_toggle_handler)
@@ -291,7 +291,7 @@ class UserDataPage(Adw.BreakpointBin):
self.sort_size.connect("clicked", self.sort_button_handler) self.sort_size.connect("clicked", self.sort_button_handler)
self.bpt.connect("apply", self.breakpoint_handler, True) self.bpt.connect("apply", self.breakpoint_handler, True)
self.bpt.connect("unapply", self.breakpoint_handler, False) self.bpt.connect("unapply", self.breakpoint_handler, False)
# Apply again # Apply again
self.loading_view.set_content(LoadingStatus(_("Loading User Data"), _("This should only take a moment"))) self.loading_view.set_content(LoadingStatus(_("Loading User Data"), _("This should only take a moment")))
self.search_bar.set_key_capture_widget(main_window) self.search_bar.set_key_capture_widget(main_window)

View File

@@ -5,4 +5,4 @@ revision = main
depth = 1 depth = 1
[provide] [provide]
program_names = blueprint-compiler program_names = blueprint-compiler