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
with:
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
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -139,7 +139,7 @@
<li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li>
</ul>
<p>Previous Releases's Bug Fixes</p>
<p>Previous Releases's Bug Fixes</p>
<ul>
<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>
@@ -158,7 +158,7 @@
<li>Update to GNOME 46 GTK Technologies</li>
<li>Updated translations</li>
</ul>
<p>Bug Fixes</p>
<p>Bug Fixes</p>
<ul>
<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>
@@ -168,7 +168,7 @@
</release>
<release version="1.5.1" date="2024-3-8" timestamp="1709921475">
<description translate="no">
<p>Bug Fixes</p>
<p>Bug Fixes</p>
<ul>
<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>
@@ -189,7 +189,7 @@
<li>Period, 0 to 9, and underscores are now allowed in new Custom Remote names</li>
<li>Updated translations</li>
</ul>
<p>Bug Fixes</p>
<p>Bug Fixes</p>
<ul>
<li>Hide Show Disabled Remotes button when there aren't any</li>
<li>Fix Batch Snapshots accidentally triggering Select All</li>

View File

@@ -18,21 +18,21 @@ class ChangeVersionPage(Adw.NavigationPage):
versions_group = gtc()
action_bar = gtc()
apply_button = gtc()
selected_commit = None
failure = None
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']} "
installation = self.package.info["installation"]
if installation == "user" or installation == "system":
script += f"--{installation}"
else:
script += f"--installation={installation}"
cmd.append(script)
commits = []
changes = []
dates = []
@@ -55,11 +55,11 @@ class ChangeVersionPage(Adw.NavigationPage):
except Exception as e:
self.failure = str(e)
return
if not (len(commits) == len(changes) == len(dates)):
self.failure = "Commits, Changes, and Dates are not of equivalent length"
return
def idle(*args):
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])}")
@@ -73,28 +73,28 @@ class ChangeVersionPage(Adw.NavigationPage):
check.set_group(self.root_group_check_button)
row.set_activatable_widget(check)
row.add_prefix(check)
self.versions_group.add(row)
GLib.idle_add(idle)
def set_commit(self, commit):
self.selected_commit = commit
def get_commits_callback(self, *args):
if not self.failure is None:
self.toast_overlay.add_toast(ErrorToast(_("Could not get versions"), self.failure).toast)
else:
self.scrolled_window.set_child(self.versions_clamp)
def callback(self, did_error):
HostInfo.main_window.refresh_handler()
if not did_error:
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):
HostInfo.main_window.toast_overlay.add_toast(ErrorToast(user_facing_label, error_message).toast)
def on_apply(self, *args):
if ChangeVersionWorker.change_version(
self.mask_row.get_active(),
@@ -104,21 +104,21 @@ class ChangeVersionPage(Adw.NavigationPage):
self.error_callback,
):
self.packages_page.set_status(self.packages_page.changing_version)
def __init__(self, packages_page, package, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.packages_page = packages_page
self.package = package
# Apply
pkg_name = package.info["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.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)
# Connections
self.root_group_check_button.connect("toggled", lambda *_: self.action_bar.set_revealed(True))
self.apply_button.connect("clicked", self.on_apply)

View File

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

View File

@@ -6,15 +6,15 @@ from .error_toast import ErrorToast
class AttemptInstallDialog(Adw.AlertDialog):
__gtype_name__ = "AttemptInstallDialog"
gtc = Gtk.Template.Child
preferences_group = gtc()
def generate_list(self):
for installation, remotes in HostInfo.remotes.items():
for remote in remotes:
if remote.disabled:
continue
row = Adw.ActionRow(title=remote.title, subtitle=_("Installation: {}").format(installation))
row.remote_name = remote.name
row.remote_installation = installation
@@ -28,20 +28,20 @@ class AttemptInstallDialog(Adw.AlertDialog):
button.set_group(self.rows[0].check_button)
else:
button.activate()
def on_response(self, dialog, response):
if response != "continue":
if not self.callback is None:
self.callback(False)
return
active_row = None
for row in self.rows:
if row.check_button.get_active():
active_row = row
break
if not active_row is None:
install_page = HostInfo.main_window.pages[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:
self.callback(False)
def __init__(self, package_names, callback=None, **kwargs):
super().__init__(**kwargs)
# Extra Object Creation
self.rows = []
self.package_names = package_names
self.callback = callback
# Apply
self.generate_list()
if len(self.rows) == 1:
self.set_extra_child(None)
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)
self.present(HostInfo.main_window)
# Connections
self.connect("response", self.on_response)

View File

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

View File

@@ -5,7 +5,7 @@ from .host_info import HostInfo
class InstallationChooser(Adw.PreferencesGroup):
__gtype_name__ = 'InstallationChooser'
gtc = Gtk.Template.Child
user_row = gtc()
system_row = gtc()
single_row = gtc()
@@ -14,14 +14,14 @@ class InstallationChooser(Adw.PreferencesGroup):
system_check = gtc()
single_check = gtc()
choice_check = gtc()
def get_installation(self):
for button, func in self.check_buttons.items():
if button.get_active():
return func()
return "" # Case for when no button is active (which shouldn't happen)
def set_content_strings(self, content_name, is_plural):
if is_plural:
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.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))
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.check_buttons = {
self.user_check : lambda: "user",
self.system_check: lambda: "system",
self.single_check: self.single_row.get_title,
self.choice_check: lambda: self.choice_row.get_selected_item().get_string(),
}
# Apply
custom_installations = []
for installation in HostInfo.installations:
if installation.startswith("user") or installation.startswith("system"):
continue
custom_installations.append(installation)
if len(custom_installations) == 1:
self.single_row.set_visible(True)
self.single_row.set_title(custom_installations[0])
elif len(custom_installations) > 1:
self.choice_row.set_visible(True)
self.choice_row.set_model(Gtk.StringList(strings=custom_installations))
# Connections
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):
super().__init__(**kwargs)
# Extra Object Creation
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()
class Flatpak:
def open_app(self, callback=None):
self.failed_app_run = None
def thread(*args):
@@ -58,10 +57,10 @@ class Flatpak:
cmd.append(f"--{installation}")
else:
cmd.append(f"--installation={installation}")
if not should_mask:
cmd.append("--remove")
try:
subprocess.run(cmd, check=True, capture_output=True, text=True)
self.is_masked = should_mask
@@ -76,7 +75,7 @@ class Flatpak:
self.failed_pin = None
if not self.is_runtime:
self.failed_pin = "Cannot pin an application"
def thread(*args):
cmd = ['flatpak-spawn', '--host', 'flatpak', 'pin', f"runtime/{self.info['ref']}"]
installation = self.info["installation"]
@@ -99,7 +98,7 @@ class Flatpak:
def uninstall(self, callee_callback=None):
self.failed_uninstall = None
def callback(*args):
HostInfo.main_window.remove_refresh_lockout("uninstalling packages")
if not callee_callback is None:
@@ -149,16 +148,16 @@ class Flatpak:
first = lines.pop(0)
if " - " in first:
cli_info["description"] = first.split(" - ")[1]
# Handle descriptions that contain newlines
while (line := lines.pop(0)) and not ":" in line:
if len(line) > 0:
cli_info["description"] += f" {line}"
for i, word in enumerate(lines):
if not ":" in word:
continue
word = word.strip().split(": ", 1)
if len(word) < 2:
continue
@@ -248,7 +247,7 @@ class HostInfo:
lines = output.strip().split("\n")
for i in lines:
icon_theme.add_search_path(f"{i}/exports/share/icons")
flatpaks = []
id_to_flatpak = {}
ref_to_flatpak = {}
@@ -270,7 +269,6 @@ class HostInfo:
this.dependent_runtime_refs.clear()
def thread(task, *args):
# Remotes
def remote_info(installation):
cmd = ['flatpak-spawn', '--host',
@@ -353,7 +351,7 @@ class HostInfo:
this.flatpaks.append(package)
this.id_to_flatpak[package.info["id"]] = package
this.ref_to_flatpak[package.info["ref"]] = package
# Dependent Runtimes
output = subprocess.run(
['flatpak-spawn', '--host',
@@ -366,19 +364,19 @@ class HostInfo:
split_line = line.split("\t")
if len(split_line) < 2 or split_line[0] == '':
continue
package = this.flatpaks[index]
if package.is_runtime:
continue
runtime = split_line[0]
package.dependent_runtime = this.ref_to_flatpak[runtime]
if not runtime in this.dependent_runtime_refs:
this.dependent_runtime_refs.append(runtime)
except subprocess.CalledProcessError as cpe:
this.main_window.toast_overlay.add_toast(ErrorToast(_("Could not load packages"), cpe.stderr).toast)
except Exception as e:
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,10 +32,10 @@ from .error_toast import ErrorToast
class WarehouseApplication(Adw.Application):
"""The main application singleton class."""
troubleshooting = "OS: {os}\nWarehouse version: {wv}\nGTK: {gtk}\nlibadwaita: {adw}\nApp ID: {app_id}\nProfile: {profile}\nLanguage: {lang}"
version = Config.VERSION
def __init__(self):
super().__init__(
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("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("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-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-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-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"])
@@ -61,9 +61,9 @@ class WarehouseApplication(Adw.Application):
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("leftover-data-view", lambda *_: self.on_data_view_shortcut(False), ["<Alt>2"])
self.is_dialog_open = False
gtk_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")
lang = GLib.environ_getenv(GLib.get_environ(), "LANG")
self.troubleshooting = self.troubleshooting.format(
os=os_string,
wv=self.version,
@@ -90,22 +90,22 @@ class WarehouseApplication(Adw.Application):
app_id=self.get_application_id(),
lang=lang,
)
def on_open_files_shortcut(self, *args):
window = self.props.active_window
def file_choose_callback(object, result):
try:
files = object.open_multiple_finish(result)
if not files:
window.toast_overlay.add_toast(ErrorToast(_("Could not add files"), _("No files were found")).toast)
return
window.on_file_drop(None, files, None, None)
except GLib.GError as gle:
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)
file_filter = Gtk.FileFilter(name=_("Flatpaks & Remotes"))
file_filter.add_suffix("flatpak")
file_filter.add_suffix("flatpakref")
@@ -116,63 +116,63 @@ class WarehouseApplication(Adw.Application):
file_chooser.set_filters(filters)
file_chooser.set_default_filter(file_filter)
file_chooser.open_multiple(window, None, file_choose_callback)
def on_toggle_select_mode_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().select_button
button.set_active(not button.get_active())
except AttributeError:
pass
def on_search_mode_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().search_button
button.set_active(True)
except AttributeError:
pass
def on_filter_shortcut(self, *args):
try:
button = self.props.active_window.stack.get_visible_child().filter_button
button.set_active(not button.get_active())
except AttributeError:
pass
try:
button = self.props.active_window.stack.get_visible_child().sort_button
button.set_active(True)
except AttributeError:
pass
try:
button = self.props.active_window.stack.get_visible_child().show_disabled_button
if button.get_visible():
button.set_active(not button.get_active())
except AttributeError:
pass
def on_new_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child()
try:
page.new_custom_handler()
except AttributeError:
pass
try:
page.on_new()
except AttributeError:
pass
def on_delete_shortcut(self, *args):
page = self.props.active_window.stack.get_visible_child()
try:
if not page.select_button.get_active():
return
page.select_trash_handler()
except AttributeError:
pass
def on_data_view_shortcut(self, is_active):
page = self.props.active_window.stack.get_visible_child()
try:
@@ -181,10 +181,10 @@ class WarehouseApplication(Adw.Application):
page.stack.set_visible_child(adp if is_active else ldp)
except AttributeError:
pass
def do_activate(self):
"""Called when the application is activated.
We raise the application's main window, creating it if
necessary.
"""
@@ -192,7 +192,7 @@ class WarehouseApplication(Adw.Application):
if not win:
win = WarehouseWindow(application=self)
win.present()
def on_about_action(self, widget, _a):
"""Callback for the app.about action."""
about = Adw.AboutDialog(
@@ -233,14 +233,14 @@ class WarehouseApplication(Adw.Application):
],
)
about.present(self.props.active_window)
def on_preferences_action(self, widget, _):
"""Callback for the app.preferences action."""
print("app.preferences action activated")
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
Args:
name: the name of the action
callback: the function to be called when the action is activated
@@ -251,7 +251,7 @@ class WarehouseApplication(Adw.Application):
self.add_action(action)
if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version):
"""The application's entry point."""
app = WarehouseApplication()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ class RemoteRow(Adw.ActionRow):
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))
return
self.remove_css_class("warning")
self.set_icon_name("")
self.set_tooltip_text("")
@@ -60,7 +60,7 @@ class RemoteRow(Adw.ActionRow):
if self.parent_page.total_disabled == 0:
self.parent_page.show_disabled_button.set_active(False)
self.parent_page.show_disabled_button.set_visible(False)
Gio.Task.new(None, None, callback).run_in_thread(thread)
def disable_remote_handler(self, *args):
@@ -115,7 +115,7 @@ class RemoteRow(Adw.ActionRow):
def on_response(_, response):
if response != "continue":
return
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))

View File

@@ -22,7 +22,6 @@ class NewRemoteRow(Adw.ActionRow):
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/remotes_page/remotes_page.ui")
class RemotesPage(Adw.NavigationPage):
# Preselected 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
instance = None
page_name = "remotes"
def start_loading(self):
self.search_button.set_active(False)
self.status_stack.set_visible_child(self.loading_view)
@@ -137,7 +136,7 @@ class RemotesPage(Adw.NavigationPage):
if row.get_visible():
any_visible = True
break
self.none_visible.set_visible(not any_visible)
def filter_remote(self, row):
@@ -150,7 +149,7 @@ class RemotesPage(Adw.NavigationPage):
packages_page.apply_filters()
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))))
def remove_remote(self, row):
error = [None]
def thread(*args):
@@ -182,7 +181,7 @@ class RemotesPage(Adw.NavigationPage):
def on_response(_, response):
if response != "continue":
return
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))
@@ -196,7 +195,7 @@ class RemotesPage(Adw.NavigationPage):
text = entry.get_text().lower()
total = 0
show_disabled = self.show_disabled_button.get_active()
for row in self.current_remote_rows:
title_match = text in row.get_title().lower()
subtitle_match = text in row.get_subtitle().lower()
@@ -209,7 +208,7 @@ class RemotesPage(Adw.NavigationPage):
return
self.stack.set_visible_child(self.content_page if total > 0 else self.no_results)
def local_file_handler(self, path):
try:
name = path.split("/")[-1].split(".")[0]
@@ -260,7 +259,7 @@ class RemotesPage(Adw.NavigationPage):
total_visible += 1
self.none_visible.set_visible(total_visible == 0)
def new_custom_handler(self, *args):
AddRemoteDialog(self.main_window, self).present(self.main_window)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ class DataSubpage(Gtk.Stack):
gtc = Gtk.Template.Child
scrolled_window = gtc()
label_box = gtc()
subtitle_size_box = gtc()
title = gtc()
@@ -100,7 +100,7 @@ class DataSubpage(Gtk.Stack):
self.selected_boxes.remove(box)
except ValueError:
pass
total = len(self.selected_boxes)
self.subtitle.set_visible(not 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.check_button.connect("toggled", lambda *_, box=box: self.box_select_handler(box))
self.flow_box.append(box)
idx = 0
while box := self.flow_box.get_child_at_index(idx):
idx += 1

View File

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

View File

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