diff --git a/src/clock-alt-symbolic.svg b/src/clock-alt-symbolic.svg
new file mode 100644
index 0000000..2cb076c
--- /dev/null
+++ b/src/clock-alt-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/common.py b/src/common.py
index a7f591f..9ccd1d5 100644
--- a/src/common.py
+++ b/src/common.py
@@ -15,11 +15,13 @@ class myUtils:
def trashFolder(self, path):
if not os.path.exists(path):
+ print("error in common.trashFolder: path does not exists. path =", path)
return 1
try:
- subprocess.run(["flatpak-spawn", "--host", "gio", "trash", path], capture_output=True, check=True, env=self.new_env)
+ subprocess.run(["gio", "trash", path], capture_output=False, check=True, env=self.new_env)
return 0
- except subprocess.CalledProcessError:
+ except subprocess.CalledProcessError as e:
+ print("error in common.trashFolder: CalledProcessError:", e)
return 2
def getSizeWithFormat(self, path):
diff --git a/src/remotes.blp b/src/remotes.blp
index 6a748db..b97837c 100644
--- a/src/remotes.blp
+++ b/src/remotes.blp
@@ -17,37 +17,37 @@ template RemotesWindow : Adw.Window {
}
content:
Adw.ToastOverlay toast_overlay {
- Stack stack {
- Overlay main_overlay {
- [overlay]
- ProgressBar progress_bar {
- visible: false;
- pulse-step: 0.7;
- can-target: false;
- styles ["osd"]
- }
- ScrolledWindow scroll {
- vexpand: true;
- Adw.Clamp{
- ListBox remotes_list {
- margin-top: 12;
- margin-bottom: 12;
- margin-start: 12;
- margin-end: 12;
- hexpand: true;
- valign: start;
- selection-mode: none;
- styles["boxed-list"]
+ Stack stack {
+ Overlay main_overlay {
+ [overlay]
+ ProgressBar progress_bar {
+ visible: false;
+ pulse-step: 0.7;
+ can-target: false;
+ styles ["osd"]
+ }
+ ScrolledWindow scroll {
+ vexpand: true;
+ Adw.Clamp{
+ ListBox remotes_list {
+ margin-top: 12;
+ margin-bottom: 12;
+ margin-start: 12;
+ margin-end: 12;
+ hexpand: true;
+ valign: start;
+ selection-mode: none;
+ styles["boxed-list"]
+ }
}
}
}
+ Adw.StatusPage no_remotes {
+ icon-name: "error-symbolic";
+ title: _("No Remotes");
+ description: _("Warehouse cannot see the list of remotes or the system has no remotes added");
+ }
}
- Adw.StatusPage no_remotes {
- icon-name: "error-symbolic";
- title: _("No Remotes");
- description: _("Warehouse cannot see the list of remotes or the system has no remotes added");
- }
- }
};
}
}
\ No newline at end of file
diff --git a/src/snapshots.blp b/src/snapshots.blp
index 935dcc7..6451c52 100644
--- a/src/snapshots.blp
+++ b/src/snapshots.blp
@@ -3,43 +3,58 @@ using Adw 1;
template SnapshotsWindow : Adw.Window {
default-width: 500;
- default-height: 450;
+ default-height: 455;
modal: true;
Adw.ToolbarView main_toolbar_view {
[top]
HeaderBar header_bar {
[start]
- Button new_button {
+ Button new_snapshot {
Adw.ButtonContent {
label: _("New Snapshot");
icon-name: "plus-large-symbolic";
}
}
+ [end]
+ Button oepn_folder_button {
+ icon-name: "document-open-symbolic";
+ tooltip-text: _("Open Snapshots Folder");
+ }
}
content:
Adw.ToastOverlay toast_overlay {
- Stack main_stack {
- Overlay main_overlay {
- [overlay]
- ProgressBar progress_bar {
- pulse-step: 0.7;
- can-target: false;
- styles["osd"]
- }
-
- Adw.PreferencesPage outerbox {
-
- Adw.PreferencesGroup {
- Adw.SwitchRow mask_row {
- title: _("Disable Updates");
- active: true;
+ Overlay main_overlay {
+ [overlay]
+ ProgressBar progress_bar {
+ pulse-step: 0.7;
+ can-target: false;
+ visible: false;
+ styles["osd"]
+ }
+ Stack main_stack {
+ ScrolledWindow outerbox {
+ Adw.Clamp {
+ ListBox snapshots_group {
+ margin-top: 12;
+ margin-bottom: 12;
+ margin-start: 12;
+ margin-end: 12;
+ valign: start;
+ selection-mode: none;
+ styles["boxed-list"]
}
}
+ }
+ Adw.StatusPage no_snapshots {
+ title: _("No Snapshots");
+ description: _("Snapshots are backups of the app's user data. They can be reapplied at any time.");
+ icon-name: "clock-alt-symbolic";
- Adw.PreferencesGroup versions_group {
- title: _("Select a Release");
- description: _("This will uninstall the current release and install the chosen one instead. Note that downgrading can cause issues.");
+ Button new_snapshot_pill {
+ label: _("New Snapshot");
+ halign: center;
+ styles["pill", "suggested-action"]
}
}
}
diff --git a/src/snapshots_window.py b/src/snapshots_window.py
index 8a7c076..8a3cfe9 100644
--- a/src/snapshots_window.py
+++ b/src/snapshots_window.py
@@ -3,6 +3,7 @@ from .common import myUtils
import subprocess
import os
import pathlib
+import time
@Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshots.ui")
class SnapshotsWindow(Adw.Window):
@@ -10,8 +11,158 @@ class SnapshotsWindow(Adw.Window):
new_env = dict( os.environ )
new_env['LC_ALL'] = 'C'
+ host_home = str(pathlib.Path.home())
+ user_data_path = host_home + "/.var/app/"
+ snapshots_path = host_home + "/.var/app/io.github.flattool.Warehouse/data/Snapshots/"
+
+ snapshots_group = Gtk.Template.Child()
+ main_stack = Gtk.Template.Child()
+ no_snapshots = Gtk.Template.Child()
+ new_snapshot = Gtk.Template.Child()
+ new_snapshot_pill = Gtk.Template.Child()
+ oepn_folder_button = Gtk.Template.Child()
+ toast_overlay = Gtk.Template.Child()
+ outerbox = Gtk.Template.Child()
+ progress_bar = Gtk.Template.Child()
+ should_pulse = False
+
+ def pulser(self):
+ self.progress_bar.pulse()
+ GLib.timeout_add(500, self.pulser)
+
+ def generateList(self):
+ if not os.path.exists(self.snapshots_of_app_path):
+ # Show no snapshots page if the folder is not there
+ self.main_stack.set_visible_child(self.no_snapshots)
+ self.oepn_folder_button.set_sensitive(False)
+ return
+
+ snapshot_files = os.listdir(self.snapshots_of_app_path)
+ to_trash = []
+
+ for i in range(len(snapshot_files)):
+ if not snapshot_files[i].endswith(".tar.zst"):
+ # Find all files that aren't snapshots
+ to_trash.append(snapshot_files[i])
+
+ for i in range(len(to_trash)):
+ # Delete all files that aren't snapshots
+ a = self.my_utils.trashFolder(f"{self.snapshots_of_app_path}{to_trash[i]}")
+ if a == 0:
+ snapshot_files.remove(to_trash[i])
+
+ if len(snapshot_files) == 0:
+ self.main_stack.set_visible_child(self.no_snapshots)
+ return
+
+ for i in range(len(snapshot_files)):
+ self.create_row(snapshot_files[i])
+
+ def create_row(self, file):
+ size = self.my_utils.getSizeWithFormat(self.snapshots_of_app_path + file)
+ split_file = file.removesuffix(".tar.zst").split("_")
+ time = GLib.DateTime.new_from_unix_local(int(split_file[0])).format("%x %X")
+ row = Adw.ActionRow(title=time, subtitle=size)
+
+ label = Gtk.Label(label=_("Version {}").format(split_file[1]), hexpand=True, wrap=True, justify=Gtk.Justification.RIGHT)
+ row.add_suffix(label)
+
+ apply = Gtk.Button(icon_name="check-plain-symbolic", valign=Gtk.Align.CENTER)
+ apply.add_css_class("flat")
+ row.add_suffix(apply)
+
+ trash = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER)
+ trash.connect("clicked", self.trash_snapshot, file, row)
+ trash.add_css_class("flat")
+ row.add_suffix(trash)
+ self.snapshots_group.insert(row, 0)
+ self.main_stack.set_visible_child(self.outerbox)
+ self.oepn_folder_button.set_sensitive(True)
+
+ def trash_snapshot(self, button, file, row):
+ def on_response(dialog, response, func):
+ if response == "cancel":
+ return
+ a = self.my_utils.trashFolder(self.snapshots_of_app_path + file)
+ if a == 0:
+ self.snapshots_group.remove(row)
+ if not self.snapshots_group.get_row_at_index(0):
+ self.main_stack.set_visible_child(self.no_snapshots)
+ self.my_utils.trashFolder(self.snapshots_of_app_path)
+ self.oepn_folder_button.set_sensitive(False)
+ else:
+ self.toast_overlay.add_toast(Adw.Toast.new(_("Could not trash snapshot")))
+
+ dialog = Adw.MessageDialog.new(self, _("Trash Snapshot?"), _("This snapshot and its contents will be sent to the trash."))
+ dialog.add_response("cancel", _("Cancel"))
+ dialog.set_close_response("cancel")
+ dialog.add_response("continue", _("Trash Snapshot"))
+ dialog.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE)
+ dialog.connect("response", on_response, dialog.choose_finish)
+ dialog.present()
+
+ def createSnapshot(self):
+ epoch = int(time.time())
+
+ def thread():
+ subprocess.run(
+ ['tar', 'caf', f"{self.snapshots_of_app_path}{epoch}_{self.app_version}.tar.zst", "-C", f"{self.app_user_data}", "."],
+ check=True, env=self.new_env
+ )
+
+ # `tar -tf filepath` to see the contents of a tar file
+
+ def callback():
+ self.create_row(f"{epoch}_{self.app_version}.tar.zst")
+ self.new_snapshot.set_sensitive(True)
+ self.new_snapshot_pill.set_sensitive(True)
+ self.progress_bar.set_visible(False)
+
+ if not os.path.exists(self.snapshots_of_app_path):
+ file = Gio.File.new_for_path(self.snapshots_of_app_path)
+ file.make_directory()
+
+ self.new_snapshot.set_sensitive(False)
+ self.new_snapshot_pill.set_sensitive(False)
+ self.progress_bar.set_visible(True)
+
+ task = Gio.Task.new(None, None, lambda *_: callback())
+ task.run_in_thread(lambda *_: thread())
+
+ def open_button_handler(self, widget, path):
+ try:
+ Gio.AppInfo.launch_default_for_uri(f"file://{path}", None)
+ except GLib.GError:
+ self.toast_overlay.add_toast(Adw.Toast.new(_("Could not open folder")))
def __init__(self, parent_window, flatpak_row, **kwargs):
super().__init__(**kwargs)
- self.present()
\ No newline at end of file
+ # Variables
+ self.my_utils = myUtils(self)
+ self.app_name = flatpak_row[0]
+ self.app_id = flatpak_row[2]
+ self.app_version = flatpak_row[3]
+ self.app_ref = flatpak_row[8]
+ self.snapshots_of_app_path = self.snapshots_path + self.app_id + "/"
+ self.app_user_data = self.user_data_path + self.app_id + "/"
+
+ if self.app_version == "" or self.app_version == "-" or self.app_version == None:
+ self.app_version = 0.0
+
+ if not os.path.exists(self.snapshots_path):
+ # Create snapshots folder if none exists
+ file = Gio.File.new_for_path(self.snapshots_path)
+ file.make_directory()
+
+ # Calls
+ self.generateList()
+ self.oepn_folder_button.connect("clicked", self.open_button_handler, self.snapshots_of_app_path)
+ self.new_snapshot.connect("clicked", lambda *_: self.createSnapshot())
+ self.new_snapshot_pill.connect("clicked", lambda *_: self.createSnapshot())
+ self.pulser()
+
+ # Window stuffs
+ self.set_title(_("{} Snapshots").format(self.app_name))
+ self.set_transient_for(parent_window)
+ self.set_size_request(0, 230)
\ No newline at end of file
diff --git a/src/warehouse.gresource.xml b/src/warehouse.gresource.xml
index 97acb5c..134896a 100644
--- a/src/warehouse.gresource.xml
+++ b/src/warehouse.gresource.xml
@@ -25,5 +25,6 @@
funnel-symbolic.svg
right-large-symbolic.svg
view-more-symbolic.svg
+ clock-alt-symbolic.svg
diff --git a/src/window.py b/src/window.py
index 4db57c8..2dd95a0 100644
--- a/src/window.py
+++ b/src/window.py
@@ -381,7 +381,7 @@ class WarehouseWindow(Adw.ApplicationWindow):
open_data_item.set_attribute_value("hidden-when", GLib.Variant.new_string("action-disabled"))
data_menu_model.append_item(open_data_item)
- self.create_action(("snapshot" + str(index)), lambda *_, row=self.flatpak_rows[index]: SnapshotsWindow(self, row))
+ self.create_action(("snapshot" + str(index)), lambda *_, row=self.flatpak_rows[index][6]: SnapshotsWindow(self, row).present())
snapshot_item = Gio.MenuItem.new(_("Manage Snapshots"), f"win.snapshot{index}")
snapshot_item.set_attribute_value("hidden-when", GLib.Variant.new_string("action-dsiabled"))
data_menu_model.append_item(snapshot_item)